diff --git a/eng/testing/tests.wasm.targets b/eng/testing/tests.wasm.targets index 9f8b2eab5ec124..84f7416ea3d697 100644 --- a/eng/testing/tests.wasm.targets +++ b/eng/testing/tests.wasm.targets @@ -150,6 +150,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..0fd2653b0a3087 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,9 @@ + + + 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..7695fa4d0b3598 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Codegen.cs @@ -0,0 +1,523 @@ +// 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; + + 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 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", + }; + } + } + + 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()); + } + } + + 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 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))" }, + }; + + 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 thisRoot = token.thisArgRoot;"); + output.AppendLine($"const converter_{state.MarshalString.Key} = converter.compiled_function;"); + output.AppendLine("const unboxBuffer = token.unboxBuffer;"); + output.AppendLine("const unboxBufferSize = token.unboxBufferSize;"); + // get_api here will also ensure that every function we reference is available and do + // the check now at construction time instead of later when the bound method is called + foreach (var key in state.ClosureReferences) + output.AppendLine($"const {key} = get_api('{key}');"); + + output.Append($"function {name} ("); + for (int i = 0; i < length; i++) { + if (i < (length - 1)) + output.Append($"arg{i}, "); + else + output.AppendLine($"arg{i}) {{"); + } + if (length == 0) + output.AppendLine(") {"); + + output.AppendLine(" _create_temp_frame();"); + output.AppendLine(" let resultRoot = token.scratchResultRoot;"); + output.AppendLine(" let exceptionRoot = token.scratchExceptionRoot;"); + output.AppendLine(" token.scratchResultRoot = null;"); + output.AppendLine(" token.scratchExceptionRoot = null;"); + output.AppendLine(" if (resultRoot === null)"); + output.AppendLine(" resultRoot = mono_wasm_new_root();"); + output.AppendLine(" if (exceptionRoot === null)"); + output.AppendLine(" exceptionRoot = mono_wasm_new_root();"); + output.AppendLine(); + + output.AppendLine( " let argsRootBuffer = _get_args_root_buffer_for_method_call(converter, token);"); + output.AppendLine( " let scratchBuffer = _get_buffer_for_method_call(converter, token);"); + output.AppendLine( " let buffer = 0;"); + output.AppendLine( " try {"); + output.AppendLine($" buffer = converter_{state.MarshalString.Key}("); + output.AppendLine( " scratchBuffer, argsRootBuffer, method,"); + for (int i = 0; i < length; i++) { + if (i < (length - 1)) + output.AppendLine($" arg{i},"); + else + output.AppendLine($" arg{i}"); + } + output.AppendLine(" );"); + output.AppendLine(); + + output.AppendLine(" resultRoot.value = invoke_method(method, thisRoot ? thisRoot.value : 0, buffer, exceptionRoot.get_address());"); + output.AppendLine(" _handle_exception_for_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(); + + if (state.MarshalString.RawReturnValue) + output.AppendLine(" return resultRoot.value;"); + else if ((state.Method?.ReturnType ?? typeof(void)) == typeof(void)) + output.AppendLine(" return;"); + else + GenerateFastUnboxBlock(state); + + output.AppendLine(" } finally {"); + output.AppendLine(" _teardown_after_call(converter, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);"); + output.AppendLine(" }"); + output.AppendLine("};"); + output.AppendLine(); + output.AppendLine($"return {name};"); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs new file mode 100644 index 00000000000000..c8e0a12090e63b --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeMarshaler.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static class DateTimeMarshaler + { + public static string JavaScriptToInterchangeTransform => @" + switch (typeof (value)) { + case 'number': + return value; + default: + if (value instanceof Date) { + return value.valueOf(); + } else + throw new Error('Value must be a number (msecs since unix epoch), or a Date'); + } +"; + public static string InterchangeToJavaScriptTransform => "return new Date(value)"; + + public static DateTime FromJavaScript (double msecsSinceEpoch) + { + return DateTimeOffset.FromUnixTimeMilliseconds((long)msecsSinceEpoch).UtcDateTime; + } + + public static double ToJavaScript (in DateTime dt) + { + return (double)new DateTimeOffset(dt).ToUnixTimeMilliseconds(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs new file mode 100644 index 00000000000000..971de1de202d50 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/DateTimeOffsetMarshaler.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public static class DateTimeOffsetMarshaler + { + public static string JavaScriptToInterchangeTransform => DateTimeMarshaler.JavaScriptToInterchangeTransform; + public static string InterchangeToJavaScriptTransform => DateTimeMarshaler.InterchangeToJavaScriptTransform; + + public static DateTimeOffset FromJavaScript (double msecsSinceEpoch) + { + return DateTimeOffset.FromUnixTimeMilliseconds((long)msecsSinceEpoch); + } + + public static double ToJavaScript (in DateTimeOffset dto) + { + return (double)dto.ToUnixTimeMilliseconds(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs index 7fa68522f9d8d5..7d30cb99c6edfa 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSException.cs @@ -10,6 +10,12 @@ namespace System.Runtime.InteropServices.JavaScript /// public class JSException : Exception { + public string? Stack; + public JSException(string msg) : base(msg) { } + + public JSException(string msg, string? stack) : base(msg) { + Stack = stack; + } } } 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/JSObject.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs index 19cb346ad9bb45..9b3b4d05caa31d 100644 --- a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSObject.cs @@ -1,6 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Console = System.Diagnostics.Debug; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + namespace System.Runtime.InteropServices.JavaScript { public interface IJSObject @@ -15,6 +20,30 @@ public interface IJSObject /// public partial class JSObject : IJSObject, IDisposable { + [StructLayout(LayoutKind.Sequential)] + private struct InvokeRecord { + public object?[] Arguments; + // FIXME: Make this object? and update Invoke + public object Result; + public string? ErrorMessage; + public string? ErrorStack; + } + + [StructLayout(LayoutKind.Sequential)] + private struct GetPropertyRecord { + public object Value; + public string? ErrorMessage; + public string? ErrorStack; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SetPropertyRecord { + public object? Value; + public string? ErrorMessage; + public string? ErrorStack; + public int CreateIfNotExists; + } + /// /// Invoke a named method of the object, or throws a JSException on error. /// @@ -34,15 +63,23 @@ public partial class JSObject : IJSObject, IDisposable /// valuews. /// /// - public object Invoke(string method, params object?[] args) + // FIXME: This should be object?, but if we correct it lots of stuff breaks + public unsafe object Invoke(string method, params object?[] args) { - AssertNotDisposed(); - - object res = Interop.Runtime.InvokeJSWithArgs(JSHandle, method, args, out int exception); - if (exception != 0) - throw new JSException((string)res); - Interop.Runtime.ReleaseInFlight(res); - return res; + var record = new InvokeRecord { + Arguments = args + }; + var pRecord = (IntPtr)Unsafe.AsPointer(ref record); + var invokeResult = Runtime.InvokeJSFunctionByName("INTERNAL._JSObject_Invoke", (IntPtr)JSHandle, method, pRecord); + if (invokeResult != InvokeJSResult.Success) + throw new JSException($"Invoke result was {invokeResult}"); + else if (record.ErrorMessage != null) + throw new JSException(record.ErrorMessage, record.ErrorStack); + else { + var result = record.Result; + Interop.Runtime.ReleaseInFlight(result); + return result; + } } public struct EventListenerOptions { @@ -128,15 +165,21 @@ public void RemoveEventListener(string name, int listenerGCHandle, EventListener /// valuews. /// /// - public object GetObjectProperty(string name) + public unsafe object GetObjectProperty(string name) { - AssertNotDisposed(); - - object propertyValue = Interop.Runtime.GetObjectProperty(JSHandle, name, out int exception); - if (exception != 0) - throw new JSException((string)propertyValue); - Interop.Runtime.ReleaseInFlight(propertyValue); - return propertyValue; + var record = new GetPropertyRecord { + }; + var pRecord = (IntPtr)Unsafe.AsPointer(ref record); + var invokeResult = Runtime.InvokeJSFunctionByName("INTERNAL._JSObject_GetProperty", (IntPtr)JSHandle, name, pRecord); + if (invokeResult != InvokeJSResult.Success) + throw new JSException($"Invoke result was {invokeResult}"); + else if (record.ErrorMessage != null) + throw new JSException(record.ErrorMessage, record.ErrorStack); + else { + var result = record.Value; + Interop.Runtime.ReleaseInFlight(result); + return result; + } } /// @@ -149,14 +192,20 @@ public object GetObjectProperty(string name) /// array that will be surfaced as a typed ArrayBuffer (byte[], sbyte[], short[], ushort[], /// float[], double[]) /// Defaults to and creates the property on the javascript object if not found, if set to it will not create the property if it does not exist. If the property exists, the value is updated with the provided value. - /// - public void SetObjectProperty(string name, object value, bool createIfNotExists = true, bool hasOwnProperty = false) + /// does nothing + // FIXME: hasOwnProperty is unused. + public unsafe void SetObjectProperty(string name, object value, bool createIfNotExists = true, bool hasOwnProperty = false) { - AssertNotDisposed(); - - object setPropResult = Interop.Runtime.SetObjectProperty(JSHandle, name, value, createIfNotExists, hasOwnProperty, out int exception); - if (exception != 0) - throw new JSException($"Error setting {name} on (js-obj js '{JSHandle}')"); + var record = new SetPropertyRecord { + Value = value, + CreateIfNotExists = createIfNotExists ? 1 : 0 + }; + var pRecord = (IntPtr)Unsafe.AsPointer(ref record); + var invokeResult = Runtime.InvokeJSFunctionByName("INTERNAL._JSObject_SetProperty", (IntPtr)JSHandle, name, pRecord); + if (invokeResult != InvokeJSResult.Success) + throw new JSException($"Invoke result was {invokeResult}"); + else if (record.ErrorMessage != null) + throw new JSException(record.ErrorMessage, record.ErrorStack); } /// 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..c01fca1282379b 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,12 +1,73 @@ // 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; +using System.Runtime.CompilerServices; 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 InvokeJSResult : int { + Success = 0, + InvalidFunctionName, + FunctionNotFound, + InvalidArgumentCount, + InvalidArgumentType, + MissingArgumentType, + NullArgumentPointer, + FunctionHadReturnValue, + FunctionThrewException, + InternalError, + } + public static partial class Runtime { private const string TaskGetResultName = "get_Result"; @@ -22,6 +83,78 @@ public static string InvokeJS(string str) return Interop.Runtime.InvokeJS(str); } + /// + /// Invoke a JS function with a specified name, passing up to 3 argument(s) + /// of a specified type at a specified address. + /// NOTE: For reference types, argN must be the address of a reference to the object, not the + /// address of the object itself. This ensures that the GC can safely move the object. + /// For value types (including pointers, ints, etc) argN is the address of the value. + /// + public static InvokeJSResult InvokeJSFunctionByName ( + string internedFunctionName, int argumentCount, + Type type1, IntPtr address1, + Type type2, IntPtr address2, + Type type3, IntPtr address3 + ) { + return (InvokeJSResult)Interop.Runtime.InvokeJSFunction( + internedFunctionName, argumentCount, + type1?.TypeHandle.Value ?? IntPtr.Zero, address1, + type2?.TypeHandle.Value ?? IntPtr.Zero, address2, + type3?.TypeHandle.Value ?? IntPtr.Zero, address3 + ); + } + + public static InvokeJSResult InvokeJSFunctionByName (string internedFunctionName) { + return (InvokeJSResult)Interop.Runtime.InvokeJSFunction( + internedFunctionName, 0, + IntPtr.Zero, IntPtr.Zero, + IntPtr.Zero, IntPtr.Zero, + IntPtr.Zero, IntPtr.Zero + ); + } + + public static unsafe InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, ref T1 arg1) { + var resultCode = Interop.Runtime.InvokeJSFunction( + internedFunctionName, 1, + typeof(T1).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg1), + IntPtr.Zero, IntPtr.Zero, + IntPtr.Zero, IntPtr.Zero + ); + return (InvokeJSResult)resultCode; + } + + public static unsafe InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, ref T1 arg1, ref T2 arg2) { + var resultCode = Interop.Runtime.InvokeJSFunction( + internedFunctionName, 2, + typeof(T1).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg1), + typeof(T2).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg2), + IntPtr.Zero, IntPtr.Zero + ); + return (InvokeJSResult)resultCode; + } + + public static unsafe InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, ref T1 arg1, ref T2 arg2, ref T3 arg3) { + var resultCode = Interop.Runtime.InvokeJSFunction( + internedFunctionName, 3, + typeof(T1).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg1), + typeof(T2).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg2), + typeof(T3).TypeHandle.Value, (IntPtr)Unsafe.AsPointer(ref arg3) + ); + return (InvokeJSResult)resultCode; + } + + public static InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, T1 arg1) { + return InvokeJSFunctionByName(internedFunctionName, ref arg1); + } + + public static InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, T1 arg1, T2 arg2) { + return InvokeJSFunctionByName(internedFunctionName, ref arg1, ref arg2); + } + + public static InvokeJSResult InvokeJSFunctionByName (string internedFunctionName, T1 arg1, T2 arg2, T3 arg3) { + return InvokeJSFunctionByName(internedFunctionName, ref arg1, ref arg2, ref arg3); + } + public static Function? CompileFunction(string snippet) { return Interop.Runtime.CompileFunction(snippet); @@ -49,129 +182,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 - } - - // 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 static string GetCallSignature(IntPtr methodHandle, object objForRuntimeType) - { - IntPtrAndHandle tmp = default(IntPtrAndHandle); - tmp.ptr = methodHandle; + private static RuntimeMethodHandle GetMethodHandleFromIntPtr (IntPtr ptr) { + var temp = new IntPtrAndHandle { ptr = ptr }; + return temp.methodHandle; + } - MethodBase? mb = objForRuntimeType == null ? MethodBase.GetMethodFromHandle(tmp.handle) : MethodBase.GetMethodFromHandle(tmp.handle, Type.GetTypeHandle(objForRuntimeType)); - if (mb == null) - return string.Empty; + private static RuntimeTypeHandle GetTypeHandleFromIntPtr (IntPtr ptr) { + var temp = new IntPtrAndHandle { ptr = ptr }; + return temp.typeHandle; + } - ParameterInfo[] parms = mb.GetParameters(); - int parmsLength = parms.Length; - if (parmsLength == 0) - return string.Empty; + private static string MakeMarshalTypeRecord (Type type, MarshalType mtype) { + var result = $"{{ \"marshalType\": {(int)mtype}, " + + $"\"typePtr\": {type.TypeHandle.Value}, " + + $"\"signatureChar\": \"{GetCallSignatureCharacterForMarshalType(mtype, 'a')}\" }}"; + return result; + } - char[] res = new char[parmsLength]; + private static MethodBase? MethodFromPointers (IntPtr typePtr, IntPtr methodPtr) { + if (methodPtr == IntPtr.Zero) + return null; - for (int c = 0; c < parmsLength; c++) - { - Type t = parms[c].ParameterType; - switch (Type.GetTypeCode(t)) - { + var methodHandle = GetMethodHandleFromIntPtr(methodPtr); + + if (typePtr != IntPtr.Zero) { + var typeHandle = GetTypeHandleFromIntPtr(typePtr); + return MethodBase.GetMethodFromHandle(methodHandle, typeHandle); + } else { + return MethodBase.GetMethodFromHandle(methodHandle); + } + } + + 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 +563,9 @@ public static string GetCallSignature(IntPtr methodHandle, object objForRuntimeT return null; } - 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) + public static string ObjectToString(object? o) { - 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 +637,46 @@ 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 Codegen.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 Codegen.MarshalString(signature, method), + FriendlyName = friendlyName, + }; + Codegen.GenerateBoundMethod(state); + return state.Output.ToString(); + } + } + + 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..82a4893dec1854 --- /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 d7b18fc65e1505..2814cfa350b6cd 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..f94a18eaa3018f 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 @@ -186,8 +186,8 @@ public static async Task Iterator() return rangeIterator; "); - const int count = 500; - for (int attempt = 0; attempt < 100; attempt++) + const int count = 500, attemptCount = 100; + for (int attempt = 0; attempt < attemptCount; attempt++) { int index = 0; try @@ -226,8 +226,14 @@ public static IEnumerable ToEnumerable(this JSObject iterrator) { object value = nextResult.GetObjectProperty("value"); nextResult.Dispose(); + if (value == null) + Console.WriteLine("iter.value was null"); yield return value; nextResult = (JSObject)iterrator.Invoke("next"); + if (nextResult == null) { + Console.WriteLine("next return value was null"); + break; + } done = (bool)nextResult.GetObjectProperty("done"); } } @@ -464,7 +470,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 b3139a60a93b27..03ff5a3ba676a0 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,15 +905,15 @@ 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.finally(result => { globalThis.__test_promise_completed = true; }); " + - "t.then(result => { globalThis.__test_promise_resolved = true; " + resolvedBody + " }); " + + "t.finally(result => { globalThis.__test_promise_completed = true; }); " + + "t.then(result => { globalThis.__test_promise_resolved = true; " + resolvedBody + " }); " + "t.catch(e => { globalThis.__test_promise_failed = true; }); " ); @@ -1018,5 +1021,364 @@ 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); + } + + [Fact] + public static void InvokeByNameBasic() + { + HelperMarshal._stringResource = null; + Runtime.InvokeJS("globalThis._test_function = function () { App.call_test_method ('InvokeString', [ 'test' ], 's') }"); + var result = Runtime.InvokeJSFunctionByName("_test_function"); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal("test", HelperMarshal._stringResource); + } + + [Fact] + public static void InvokeByNameIntArgument() + { + HelperMarshal._intValue = 0; + Runtime.InvokeJS("globalThis._test_function_int = function (i) { App.call_test_method ('InvokeInt', [ i ], 'i') }"); + var result = Runtime.InvokeJSFunctionByName("_test_function_int", 7); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(7, HelperMarshal._intValue); + } + + [Fact] + public static void InvokeByNameIntPtrArgument() + { + var expected = new IntPtr(42); + HelperMarshal._intPtrValue = IntPtr.Zero; + Runtime.InvokeJS("globalThis._test_function_intptr = function (i) { App.call_test_method ('InvokeIntPtr', [ i ], 'i') }"); + var result = Runtime.InvokeJSFunctionByName("_test_function_intptr", expected); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(expected, HelperMarshal._intPtrValue); + } + + [Fact] + public static void InvokeByNameStringArgument() + { + var expected = "hello world"; + HelperMarshal._stringResource = null; + Runtime.InvokeJS("globalThis._test_function_string = function (s) { App.call_test_method ('InvokeString', [ s ], 's') }"); + var result = Runtime.InvokeJSFunctionByName("_test_function_string", expected); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(expected, HelperMarshal._stringResource); + } + + [Fact] + public static void InvokeByNameUriArgument() + { + var expected = new System.Uri("https://www.example.com/"); + HelperMarshal._uriValue = default(System.Uri); + Runtime.InvokeJS("globalThis._test_function_uri = function (uri) { App.call_test_method ('InvokeUri', [ uri ], 'u') }"); + var result = Runtime.InvokeJSFunctionByName("_test_function_uri", expected); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(expected, HelperMarshal._uriValue); + } + + [Fact] + public static void InvokeByName3Arguments() + { + int a = 3; + DateTime b = ExpectedDateTime; + string c = "hello\0world"; + HelperMarshal._intValue = 0; + HelperMarshal._dateTimeValue = default(DateTime); + HelperMarshal._stringResource = null; + Runtime.InvokeJS("globalThis._test_function_3arg = function (a, b, c) { " + + "App.call_test_method ('InvokeInt', [ a ], 'i'); " + + "App.call_test_method ('InvokeDateTime', [ b ], 'a'); " + + "App.call_test_method ('InvokeString', [ c ], 's'); " + + "}"); + var result = Runtime.InvokeJSFunctionByName("_test_function_3arg", a, b, c); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(a, HelperMarshal._intValue); + Assert.Equal(b, HelperMarshal._dateTimeValue); + Assert.Equal(c, HelperMarshal._stringResource); + } + + [Fact] + public static void InvokeByNameTraversesObjects() + { + HelperMarshal._intValue = 0; + Runtime.InvokeJS("globalThis._foo = { _bar: { _func: function (i) { App.call_test_method ('InvokeInt', [ i ], 'i') } } };"); + var result = Runtime.InvokeJSFunctionByName("_foo._bar._func", 12); + Assert.Equal(InvokeJSResult.Success, result); + Assert.Equal(12, HelperMarshal._intValue); + } + + [Fact] + public static void InvokeByNameRejectsInvalidNames() + { + // Invalid function names + Assert.Equal(InvokeJSResult.InvalidFunctionName, Runtime.InvokeJSFunctionByName(null)); + Assert.Equal(InvokeJSResult.InvalidFunctionName, Runtime.InvokeJSFunctionByName("")); + + // Function name must be interned + var temp = new string('a', 3); + Assert.Equal(InvokeJSResult.InvalidFunctionName, Runtime.InvokeJSFunctionByName(temp)); + } + + [Fact] + public static void InvokeByNameCachesValueForName() + { + Runtime.InvokeJS("globalThis._bar = undefined;"); + var result = Runtime.InvokeJSFunctionByName("_bar"); + Assert.Equal(InvokeJSResult.FunctionNotFound, result); + + // The initial value of _bar should be cached, preventing this new one from being found and called + Runtime.InvokeJS("globalThis._bar = function () { throw new Error('should not be called'); }"); + result = Runtime.InvokeJSFunctionByName("_bar"); + Assert.Equal(InvokeJSResult.FunctionNotFound, result); + } + + [Fact] + public static void InvokeByNameRejectsMisuse() + { + Runtime.InvokeJS( + // Call target should never return a value (it will be dropped) + "globalThis._misbehavior_return_value = function () { return 7; }; " + + // Call target should not throw exceptions + "globalThis._misbehavior_throw = function () { throw new Error(); }; " + ); + + Assert.Equal( + InvokeJSResult.FunctionHadReturnValue, + Runtime.InvokeJSFunctionByName("_misbehavior_return_value") + ); + + // NOTE: This is not a behavior the consumer should rely on, it exists + // to aid debugging and so that we can potentially remove the + // exception handler entirely if it improves performance + Assert.Equal( + InvokeJSResult.FunctionThrewException, + Runtime.InvokeJSFunctionByName("_misbehavior_throw") + ); + } + + [Fact] + public static void InvokeByNameRejectsOutOfRangeArgumentCounts() + { + Assert.Equal( + InvokeJSResult.InvalidArgumentCount, + Runtime.InvokeJSFunctionByName( + "nonexistent_function", 4, + null, IntPtr.Zero, + null, IntPtr.Zero, + null, IntPtr.Zero + ) + ); + + Assert.Equal( + InvokeJSResult.InvalidArgumentCount, + Runtime.InvokeJSFunctionByName( + "nonexistent_function", -1, + null, IntPtr.Zero, + null, IntPtr.Zero, + null, IntPtr.Zero + ) + ); + } + + [Fact] + public static void InvokeByNameChecksWhetherArgumentsHaveTypeInfo() + { + Assert.Equal( + InvokeJSResult.MissingArgumentType, + Runtime.InvokeJSFunctionByName( + "nonexistent_function", 2, + typeof(string), IntPtr.Zero, + null, IntPtr.Zero, + null, IntPtr.Zero + ) + ); + } + + [Fact] + public static void JSObjectInvokeProperlyRethrowsExceptions() { + Runtime.InvokeJS( + "globalThis.testObj1 = { throwingMethod: function () { throw new Error('test'); } }; " + ); + var o = (JSObject)Runtime.GetGlobalObject("testObj1"); + var exc = Assert.Throws(() => { + var res = o.Invoke("throwingMethod"); + }); + Assert.Equal("test", exc.Message); + } } } 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/mono/metadata/object.c b/src/mono/mono/metadata/object.c index 0c7240528adfa6..8bb679c2c4429c 100644 --- a/src/mono/mono/metadata/object.c +++ b/src/mono/mono/metadata/object.c @@ -6791,9 +6791,9 @@ mono_string_is_interned_lookup (MonoStringHandle str, gboolean insert, MonoError #ifdef HOST_WASM /** * mono_string_instance_is_interned: - * Searches the interned string table for the provided string instance. + * Checks to see if the string instance has its interned flag set. * \param str String to probe - * \returns TRUE if the string is interned, FALSE otherwise. + * \returns TRUE if the string instance is interned, FALSE otherwise. */ int mono_string_instance_is_interned (MonoString *str) diff --git a/src/mono/mono/sgen/sgen-memory-governor.c b/src/mono/mono/sgen/sgen-memory-governor.c index ac801a24a92f02..5e187bac9eafe6 100644 --- a/src/mono/mono/sgen/sgen-memory-governor.c +++ b/src/mono/mono/sgen/sgen-memory-governor.c @@ -319,6 +319,8 @@ sgen_memgov_collection_start (int generation) static void sgen_output_log_entry (SgenLogEntry *entry, gint64 stw_time, int generation) { + return; + char full_timing_buff [1024]; full_timing_buff [0] = '\0'; 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 ad4896e09b8310..b674d2265445c5 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -271,6 +271,13 @@ + + + + + + + + DebugLevel="$(WasmDebugLevel)" + MarshaledTypes="@(WasmMarshaledType)" + > diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 4dfe69ccac8553..76dc28513e0d29 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -10,16 +10,16 @@ const DotnetSupportLib = { $DOTNET__postset: ` let __dotnet_replacements = {scriptDirectory, readAsync, fetch: globalThis.fetch}; let __dotnet_exportedAPI = __dotnet_runtime.__initializeImportsAndExports( - { isGlobal:ENVIRONMENT_IS_GLOBAL, isNode:ENVIRONMENT_IS_NODE, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, locateFile }, + { isGlobal:ENVIRONMENT_IS_GLOBAL, isNode:ENVIRONMENT_IS_NODE, isShell:ENVIRONMENT_IS_SHELL, isWeb:ENVIRONMENT_IS_WEB, locateFile }, { mono:MONO, binding:BINDING, internal:INTERNAL, module:Module }, __dotnet_replacements); - + // here we replace things which are not exposed in another way scriptDirectory = __dotnet_replacements.scriptDirectory; readAsync = __dotnet_replacements.readAsync; var fetch = __dotnet_replacements.fetch; - if (ENVIRONMENT_IS_NODE) { - __dirname = __dotnet_replacements.scriptDirectory; + if (ENVIRONMENT_IS_NODE) { + __dirname = __dotnet_replacements.scriptDirectory; } `, }; @@ -43,6 +43,9 @@ const linked_functions = [ "mono_wasm_invoke_js", "mono_wasm_invoke_js_blazor", "mono_wasm_trace_logger", + "mono_wasm_invoke_js_marshalled", + "mono_wasm_invoke_js_unmarshalled", + "_invoke_js_function_by_qualified_name_impl", // corebindings.c "mono_wasm_invoke_js_with_args", diff --git a/src/mono/wasm/runtime/corebindings.ts b/src/mono/wasm/runtime/corebindings.ts index 9e12e3f74d8c4f..38752747e27d4d 100644 --- a/src/mono/wasm/runtime/corebindings.ts +++ b/src/mono/wasm/runtime/corebindings.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. -import { JSHandle, GCHandle, MonoObject } from "./types"; +import { JSHandle, GCHandle, MonoObject, MonoType, MonoMethod } from "./types"; import { ArgsMarshalString } from "./method-binding"; import { PromiseControl } from "./cancelable-promise"; import { runtimeHelpers } from "./imports"; @@ -24,10 +24,10 @@ const fn_signatures: [jsname: string, csname: string, signature: ArgsMarshalStri ["_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 { @@ -49,10 +49,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..064e1302b4e193 --- /dev/null +++ b/src/mono/wasm/runtime/custom-marshaler.ts @@ -0,0 +1,384 @@ +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 { + ArgsMarshalString, 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; + +export function unbox_struct_at_address (address : VoidPtr, typePtr : MonoType) : any { + if (!typePtr) + throw new Error(`Got no typePtr for struct at address ${address}`); + + const converter = _get_struct_unboxer_for_type (typePtr); + if (converter) + return converter(address); + else + throw new Error (`No CustomJavaScriptMarshaler found for type ${_get_type_name(typePtr)}`); +} + +function extract_js_obj_root_with_converter_impl (root : WasmRoot, typePtr : MonoType, unbox_buffer : VoidPtr, optional: boolean) : any { + if (root.value === MonoObjectNull) + return null; + if (!typePtr) + throw new Error(`Got no typePtr for object at address ${root.value}`); + + 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 : ArgsMarshalString, 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 b8e5ca141e2062..c7bcf015e59ac6 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 cc4341a54dd3cd..97675262dad302 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -111,20 +111,7 @@ declare class WasmRoot { toString(): string; } -declare const enum ArgsMarshal { - Int32 = "i", - Int32Enum = "j", - Int64 = "l", - Int64Enum = "k", - Float32 = "f", - Float64 = "d", - String = "s", - Char = "s", - JSObj = "o", - MONOObj = "m" -} -declare type _ExtraArgsMarshalOperators = "!" | ""; -declare type ArgsMarshalString = "" | `${ArgsMarshal}${_ExtraArgsMarshalOperators}` | `${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}` | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}` | `${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${ArgsMarshal}${_ExtraArgsMarshalOperators}`; +declare type ArgsMarshalString = string; interface MonoObject extends ManagedPointer { __brandMonoObject: "MonoObject"; @@ -151,6 +138,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; @@ -235,6 +225,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; /** @@ -249,7 +240,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; @@ -262,10 +253,11 @@ declare function mono_call_assembly_entry_point(assembly: string, args: any[], s declare function mono_wasm_load_bytes_into_heap(bytes: Uint8Array): VoidPtr; -declare type _MemOffset = number | VoidPtr | NativePointer; +declare type _MemOffset = number | NativePointer; +declare type _MemValue = number | NativePointer | ManagedPointer; 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 setU32(offset: _MemOffset, value: _MemValue): 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; @@ -293,6 +285,7 @@ declare const MONO: { mono_wasm_new_root_buffer: typeof mono_wasm_new_root_buffer; mono_wasm_new_root: typeof mono_wasm_new_root; mono_wasm_release_roots: typeof mono_wasm_release_roots; + 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..59aa4c9dc795f3 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -35,6 +35,11 @@ extern MonoString* mono_wasm_invoke_js (MonoString *str, int *is_exception); // Blazor specific custom routines - see dotnet_support.js for backing code extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *callInfo, void* arg0, void* arg1, void* arg2); +extern uint32_t _invoke_js_function_by_qualified_name_impl ( + MonoString *internedFunctionName, uint32_t argumentCount, + int* marshalTypes, MonoType **typeHandles, void **arguments +); + void mono_wasm_enable_debugging (int); int mono_wasm_marshal_type_from_mono_type (int mono_type, MonoClass *klass, MonoType *type); @@ -65,8 +70,6 @@ char *mono_method_get_full_name (MonoMethod *method); #define MARSHAL_TYPE_OBJECT 7 #define MARSHAL_TYPE_BOOL 8 #define MARSHAL_TYPE_ENUM 9 -#define MARSHAL_TYPE_DATE 20 -#define MARSHAL_TYPE_DATEOFFSET 21 #define MARSHAL_TYPE_URI 22 #define MARSHAL_TYPE_SAFEHANDLE 23 @@ -90,21 +93,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; @@ -138,6 +141,80 @@ static MonoDomain *root_domain; extern void mono_wasm_trace_logger (const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data); +#define INVOKERESULT_Success 0 +#define INVOKERESULT_InvalidFunctionName 1 // null/blank name, name not interned, or syntax error +#define INVOKERESULT_FunctionNotFound 2 // no function found matching this function name +#define INVOKERESULT_InvalidArgumentCount 3 // argument count outside valid range [0-3] +#define INVOKERESULT_InvalidArgumentType 4 // an argument was of an unsupported type +#define INVOKERESULT_MissingArgumentType 5 // an argument value was provided without a type handle +#define INVOKERESULT_NullArgumentPointer 6 // the pointer to a non-nullable argument value was 0 +#define INVOKERESULT_FunctionHadReturnValue 7 +#define INVOKERESULT_FunctionThrewException 8 +#define INVOKERESULT_InternalError 9 // an unspecified internal error occurred + +#define INVOKE_ARGUMENT_BUFFER_SIZE 3 + +static int32_t +mono_wasm_invoke_js_function_by_qualified_name ( + MonoString *internedFunctionName, int32_t argumentCount, + MonoType *type1, void *arg1, + MonoType *type2, void *arg2, + MonoType *type3, void *arg3 +) { + if (!internedFunctionName || !mono_string_instance_is_interned (internedFunctionName)) + return INVOKERESULT_InvalidFunctionName; + + if ((argumentCount < 0) || (argumentCount > INVOKE_ARGUMENT_BUFFER_SIZE)) + return INVOKERESULT_InvalidArgumentCount; + + int marshalTypes[INVOKE_ARGUMENT_BUFFER_SIZE] = {0}; + MonoType *typeHandles[INVOKE_ARGUMENT_BUFFER_SIZE] = {0}; + void *arguments[INVOKE_ARGUMENT_BUFFER_SIZE] = {0}; + + if (argumentCount > 0) { + typeHandles[0] = type1; + arguments[0] = arg1; + } + if (argumentCount > 1) { + typeHandles[1] = type2; + arguments[1] = arg2; + } + if (argumentCount > 2) { + typeHandles[2] = type3; + arguments[2] = arg3; + } + + int32_t result = 0; + MonoClass *klass; + int mono_type; + + for (int32_t i = 0; i < argumentCount; i++) { + if (typeHandles[i] == 0) { + result = INVOKERESULT_MissingArgumentType; + break; + } + + klass = mono_class_from_mono_type (typeHandles[i]); + mono_type = mono_type_get_type (typeHandles[i]); + marshalTypes[i] = mono_wasm_marshal_type_from_mono_type (mono_type, klass, typeHandles[i]); + if (marshalTypes[i] >= MARSHAL_ERROR_BUFFER_TOO_SMALL) { + result = INVOKERESULT_InvalidArgumentType; + break; + } + + // TODO: INVOKERESULT_NullArgumentPointer + } + + if (result == 0) { + result = _invoke_js_function_by_qualified_name_impl ( + internedFunctionName, argumentCount, + marshalTypes, typeHandles, arguments + ); + } + + return result; +} + static void wasm_trace_logger (const char *log_domain, const char *log_level, const char *message, mono_bool fatal, void *user_data) { @@ -158,7 +235,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); @@ -408,6 +485,8 @@ void mono_initialize_internals () // Blazor specific custom routines - see dotnet_support.js for backing code mono_add_internal_call ("WebAssembly.JSInterop.InternalCalls::InvokeJS", mono_wasm_invoke_js_blazor); + mono_add_internal_call ("Interop/Runtime::InvokeJSFunction", mono_wasm_invoke_js_function_by_qualified_name); + #ifdef CORE_BINDINGS core_initialize_internals(); #endif @@ -575,7 +654,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 +735,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 +831,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 +855,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 +882,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 +913,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 +997,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 +1007,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 +1072,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 +1182,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 +1234,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 c4a33574c5870c..d184625e320ecf 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -34,7 +34,8 @@ import { mono_wasm_set_main_args, 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"; @@ -50,7 +51,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"; @@ -68,6 +69,10 @@ import { import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; +import { + _JSObject_Invoke, _JSObject_GetProperty, _JSObject_SetProperty, + _invoke_js_function_by_qualified_name_impl +} from "./invoke-api"; const MONO = { // current "public" MONO API @@ -82,6 +87,8 @@ const MONO = { mono_wasm_new_root, mono_wasm_release_roots, + 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, @@ -256,7 +263,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(); @@ -306,6 +313,7 @@ export const __linker_exports: any = { mono_wasm_invoke_js, mono_wasm_invoke_js_blazor, mono_wasm_trace_logger, + _invoke_js_function_by_qualified_name_impl, // also keep in sync with corebindings.c mono_wasm_invoke_js_with_args, @@ -366,6 +374,32 @@ const INTERNAL: any = { mono_wasm_detach_debugger, mono_wasm_raise_debug_event, mono_wasm_runtime_is_ready: runtimeHelpers.mono_wasm_runtime_is_ready, + + // memory accessors + setI8, + setI16, + setI32, + setI64, + setU8, + setU16, + setU32, + setF32, + setF64, + getI8, + getI16, + getI32, + getI64, + getU8, + getU16, + getU32, + getF32, + getF64, + + // new invoke API + _JSObject_Invoke, + _JSObject_GetProperty, + _JSObject_SetProperty, + _invoke_js_function_by_qualified_name_impl, }; diff --git a/src/mono/wasm/runtime/invoke-api.ts b/src/mono/wasm/runtime/invoke-api.ts new file mode 100644 index 00000000000000..6b97cdfc63d220 --- /dev/null +++ b/src/mono/wasm/runtime/invoke-api.ts @@ -0,0 +1,399 @@ +import { + JSHandle, MonoArray, MonoObject, MarshalType, + MonoString, VoidPtrNull, MonoType, MonoTypeNull +} from "./types"; +import { VoidPtr } from "./types/emscripten"; +import { mono_wasm_new_root, mono_wasm_new_external_root, WasmRoot } from "./roots"; +import { _unbox_mono_obj_root, mono_array_to_js_array } from "./cs-to-js"; +import { _js_to_mono_obj } from "./js-to-cs"; +import { conv_string, js_string_to_mono_string_new } from "./strings"; +import { extract_js_obj_root_with_possible_converter, unbox_struct_at_address } from "./custom-marshaler"; +import { mono_wasm_get_jsobj_from_js_handle } from "./gc-handles"; +import { MONO, BINDING, INTERNAL } from "./imports"; +import { + getI32, + getF32, + getU32, + getF64, + setU32, +} from "./memory"; + +// Must match equivalent in Runtime.cs and driver.c +export enum InvokeJSResult { + Success = 0, + InvalidFunctionName, + FunctionNotFound, + InvalidArgumentCount, + InvalidArgumentType, + MissingArgumentType, + NullArgumentPointer, + FunctionHadReturnValue, + FunctionThrewException, + InternalError, +} + +type ResolveJSFunctionSuccessResult = { + fn : Function; + error? : never; +}; + +type ResolveJSFunctionErrorResult = { + fn? : never; + error : InvokeJSResult; +}; + +type ResolveJSFunctionResult = ResolveJSFunctionSuccessResult | ResolveJSFunctionErrorResult; + +const _invoke_js_function_cache = new Map(); + +export function _resolve_js_function_by_qualified_name (pInternedFunctionName : MonoString) : ResolveJSFunctionResult { + let res : ResolveJSFunctionResult | undefined = _invoke_js_function_cache.get(pInternedFunctionName); + if (res === undefined) { + const str = conv_string(pInternedFunctionName); + if (!str) + res = { error: InvokeJSResult.InvalidFunctionName }; + else + res = _walk_global_scope_to_find_function(str); + _invoke_js_function_cache.set(pInternedFunctionName, res); + } + return res; +} + +function try_get_special_named_object (name : string) { + switch (name) { + case "MONO": + return MONO; + case "BINDING": + return BINDING; + case "INTERNAL": + return INTERNAL; + default: + return undefined; + } +} + +export function _walk_global_scope_to_find_function (str : string) : ResolveJSFunctionResult { + let scope : any = globalThis; + let fn = scope[str]; + + if (typeof (fn) !== "function") { + const parts = str.split("."); + for (let i = 0; i < parts.length; i++) { + if (!scope) + return { error: InvokeJSResult.FunctionNotFound }; + + const part = parts[i]; + let newScope = undefined; + if (i === 0) + newScope = try_get_special_named_object(part); + if (!newScope) + newScope = scope[parts[i]]; + + scope = newScope; + } + + fn = scope; + } + + if (typeof (fn) !== "function") + return { error : InvokeJSResult.FunctionNotFound }; + else + return { fn }; +} + +class UnboxError extends Error { + errorCode : InvokeJSResult; + + constructor(errorCode : InvokeJSResult) { + super(); + this.errorCode = errorCode; + } +} + +export function _unbox_function_argument_from_heap_for_invoke ( + index : number, pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr, + root : WasmRoot | undefined +) : any { + const typeHandle = getU32(pTypeHandles + (index * 4)); + if (typeHandle === MonoTypeNull) + return undefined; + + const marshalType = getU32(pMarshalTypes + (index * 4)); + const pData = root + ? (root.value) + : getU32(pArguments + (index * 4)); + + // console.log(`index=${index}, marshalType=${marshalType}, typeHandle=${typeHandle}, pData=${pData}`); + + switch (marshalType) { + case MarshalType.INT: + return getI32(pData); + case MarshalType.POINTER: + case MarshalType.UINT32: + return getU32(pData); + case MarshalType.FP32: + return getF32(pData); + case MarshalType.FP64: + return getF64(pData); + case MarshalType.BOOL: + return (getI32(pData) !== 0); + case MarshalType.CHAR: + return String.fromCharCode(getI32(pData)); + case MarshalType.DELEGATE: + case MarshalType.TASK: + case MarshalType.OBJECT: + case MarshalType.URI: + case MarshalType.VT: + try { + if (marshalType === MarshalType.VT) + return unbox_struct_at_address(pData, typeHandle); + else + return extract_js_obj_root_with_possible_converter(>root, typeHandle, VoidPtrNull); + } catch (exc : any) { + console.error(`Uncaught error when unboxing invoke argument #${index} of type ${marshalType} at address ${(>root).value}: ${exc}\r\n${exc.stack}`); + throw new UnboxError(InvokeJSResult.InternalError); + } + case MarshalType.STRING: + case MarshalType.STRING_INTERNED: + return conv_string(((>root).value)); + default: + console.error(`Unbox/convert not implemented for marshal type ${marshalType}`); + throw new UnboxError(InvokeJSResult.InvalidArgumentType); + } +} + +const marshal_type_needs_root = new Set( + [ + MarshalType.DELEGATE, + MarshalType.TASK, + MarshalType.OBJECT, + MarshalType.URI, + MarshalType.STRING, + MarshalType.STRING_INTERNED, + ] +); + +function get_object_address_indirect (offset : number) : WasmRoot { + const ptrLocation = getU32(offset); + const result = mono_wasm_new_external_root(ptrLocation); + return result; +} + +function _allocate_root_automatically (index : number, pMarshalTypes : VoidPtr, pArguments : VoidPtr) : WasmRoot | undefined { + return marshal_type_needs_root.has(getU32(pMarshalTypes + (index * 4))) + ? get_object_address_indirect(pArguments + (index * 4)) + : undefined; +} + +function _handle_thunk_error (exc : any) : InvokeJSResult { + if (exc instanceof UnboxError) + return exc.errorCode; + console.error("invoked function threw unhandled error", exc); + return InvokeJSResult.FunctionThrewException; +} + +function _invoke_thunk_0 ( + fn : Function +) : InvokeJSResult { + try { + const invokeResult = fn(); + + if (typeof (invokeResult) !== "undefined") + return InvokeJSResult.FunctionHadReturnValue; + else + return InvokeJSResult.Success; + } catch (exc) { + return _handle_thunk_error(exc); + } +} + +function _invoke_thunk_1 ( + fn : Function, + pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr +) : InvokeJSResult { + const root0 = _allocate_root_automatically(0, pMarshalTypes, pArguments); + + try { + const arg0 = _unbox_function_argument_from_heap_for_invoke(0, pMarshalTypes, pTypeHandles, pArguments, root0); + + const invokeResult = fn(arg0); + + if (typeof (invokeResult) !== "undefined") + return InvokeJSResult.FunctionHadReturnValue; + else + return InvokeJSResult.Success; + } catch (exc) { + return _handle_thunk_error(exc); + } finally { + if (root0) + root0.release(); + } +} + +function _invoke_thunk_2 ( + fn : Function, + pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr +) : InvokeJSResult { + const root0 = _allocate_root_automatically(0, pMarshalTypes, pArguments), + root1 = _allocate_root_automatically(1, pMarshalTypes, pArguments); + + try { + const arg0 = _unbox_function_argument_from_heap_for_invoke(0, pMarshalTypes, pTypeHandles, pArguments, root0), + arg1 = _unbox_function_argument_from_heap_for_invoke(1, pMarshalTypes, pTypeHandles, pArguments, root1); + + const invokeResult = fn(arg0, arg1); + + if (typeof (invokeResult) !== "undefined") + return InvokeJSResult.FunctionHadReturnValue; + else + return InvokeJSResult.Success; + } catch (exc) { + return _handle_thunk_error(exc); + } finally { + if (root0) + root0.release(); + if (root1) + root1.release(); + } +} + +function _invoke_thunk_3 ( + fn : Function, + pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr +) : InvokeJSResult { + const root0 = _allocate_root_automatically(0, pMarshalTypes, pArguments), + root1 = _allocate_root_automatically(1, pMarshalTypes, pArguments), + root2 = _allocate_root_automatically(2, pMarshalTypes, pArguments); + + try { + const arg0 = _unbox_function_argument_from_heap_for_invoke(0, pMarshalTypes, pTypeHandles, pArguments, root0), + arg1 = _unbox_function_argument_from_heap_for_invoke(1, pMarshalTypes, pTypeHandles, pArguments, root1), + arg2 = _unbox_function_argument_from_heap_for_invoke(2, pMarshalTypes, pTypeHandles, pArguments, root2); + + const invokeResult = fn(arg0, arg1, arg2); + + if (typeof (invokeResult) !== "undefined") + return InvokeJSResult.FunctionHadReturnValue; + else + return InvokeJSResult.Success; + } catch (exc) { + return _handle_thunk_error(exc); + } finally { + if (root0) + root0.release(); + if (root1) + root1.release(); + if (root2) + root2.release(); + } +} + +function _unbox_arguments_and_invoke_js_function ( + fn : Function, argumentCount : number, + pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr +) : InvokeJSResult { + switch (argumentCount) { + case 0: + return _invoke_thunk_0(fn); + case 1: + return _invoke_thunk_1(fn, pMarshalTypes, pTypeHandles, pArguments); + case 2: + return _invoke_thunk_2(fn, pMarshalTypes, pTypeHandles, pArguments); + case 3: + return _invoke_thunk_3(fn, pMarshalTypes, pTypeHandles, pArguments); + default: + throw new Error(`Unsupported argument count ${argumentCount} (expected [0 - 3])`); + } +} + +export function _invoke_js_function_by_qualified_name_impl ( + pInternedFunctionName : MonoString, argumentCount : number, + pMarshalTypes : VoidPtr, pTypeHandles : VoidPtr, pArguments : VoidPtr +) : InvokeJSResult | any { + const resolved = _resolve_js_function_by_qualified_name(pInternedFunctionName); + if (resolved.error) + return resolved.error; + + const result = _unbox_arguments_and_invoke_js_function( + resolved.fn, argumentCount, + pMarshalTypes, pTypeHandles, pArguments + ); + + if (typeof (result) !== "number") + return InvokeJSResult.InternalError; + else + return result; +} + + +export function _JSObject_Invoke (js_handle : JSHandle, method_name : string, pRecord : VoidPtr) : void { + const pArguments = pRecord, pResult = pRecord + 4, + pErrorMessage = pRecord + 8, pErrorStack = pRecord + 12; + + try { + const obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + if (!obj) + throw new Error(`Invalid js object handle ${js_handle}`); + + const method = obj[method_name]; + if (typeof (method) !== "function") + throw new Error(`Member ${method_name} of object was not a function`); + + const args = mono_array_to_js_array(getU32(pArguments)); + const jsResult = method.apply(obj, args); + const resultPtr = _js_to_mono_obj(true, jsResult); + setU32(pResult, resultPtr); + setU32(pErrorMessage, 0); + setU32(pErrorStack, 0); + } catch (exc : any) { + setU32(pResult, 0); + setU32(pErrorMessage, js_string_to_mono_string_new(exc.message)); + setU32(pErrorStack, js_string_to_mono_string_new(exc.stack)); + } +} + +export function _JSObject_GetProperty (js_handle : JSHandle, property_name : string, pRecord : VoidPtr) : void { + const pResult = pRecord, pErrorMessage = pRecord + 4, pErrorStack = pRecord + 8; + + try { + const obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + if (!obj) + throw new Error(`Invalid js object handle ${js_handle}`); + + const value = obj[property_name]; + const resultPtr = _js_to_mono_obj(true, value); + setU32(pResult, resultPtr); + setU32(pErrorMessage, 0); + setU32(pErrorStack, 0); + } catch (exc : any) { + setU32(pResult, 0); + setU32(pErrorMessage, js_string_to_mono_string_new(exc.message)); + setU32(pErrorStack, js_string_to_mono_string_new(exc.stack)); + } +} + +export function _JSObject_SetProperty (js_handle : JSHandle, property_name : string, pRecord : VoidPtr) : void { + const pValue = pRecord, pErrorMessage = pRecord + 4, pErrorStack = pRecord + 8, + pCreate = pRecord + 12; + + const root = mono_wasm_new_root(getU32(pValue)); + try { + const value = _unbox_mono_obj_root(root); + + const obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + if (!obj) + throw new Error(`Invalid js object handle ${js_handle}`); + + const createIfNotExist = !!getU32(pCreate); + if (createIfNotExist || Object.prototype.hasOwnProperty.call(obj, property_name)) + obj[property_name] = value; + + setU32(pErrorMessage, 0); + setU32(pErrorStack, 0); + } catch (exc : any) { + setU32(pErrorMessage, js_string_to_mono_string_new(exc.message)); + setU32(pErrorStack, js_string_to_mono_string_new(exc.stack)); + } finally { + root.release(); + } +} \ No newline at end of file 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..c8760a0c7a994f 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -1,36 +1,44 @@ 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 _NumberOrPointer = number | VoidPtr | NativePointer | ManagedPointer; export function setU8(offset: _MemOffset, value: number): void { Module.HEAPU8[offset] = value; @@ -40,8 +48,8 @@ export function setU16(offset: _MemOffset, 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: _MemOffset, value: _NumberOrPointer) : void { + Module.HEAPU32[offset >>> 2] = value; } export function setI8(offset: _MemOffset, value: number): void { @@ -52,8 +60,8 @@ export function setI16(offset: _MemOffset, value: number): void { Module.HEAP16[offset >>> 1] = value; } -export function setI32(offset: _MemOffset, value: number): void { - Module.HEAP32[offset >>> 2] = value; +export function setI32 (offset: _MemOffset, value: _NumberOrPointer) : void { + Module.HEAP32[offset >>> 2] = value; } // NOTE: Accepts a number, not a BigInt, so values over Number.MAX_SAFE_INTEGER will be corrupted diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 9b4adbd2b6547a..0ea8b86fa4ddfa 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: ArgsMarshalS // 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,282 +94,212 @@ 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: 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: ArgsMarshalString): Converter { +function _get_converter_for_marshal_string(typePtr: MonoType, method: MonoMethod, args_marshal: ArgsMarshalString): 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: 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: ArgsMarshalString): 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: ArgsMarshalString, friendly_name: string): string { + const argsRoot = mono_wasm_new_root(), + nameRoot = mono_wasm_new_root(), + resultRoot = mono_wasm_new_root(), + exceptionRoot = mono_wasm_new_root(); + const generatorMethod = get_method("GenerateBoundMethod"); + const buffer = Module._malloc(64); - // ensure the indirect values are 8-byte aligned so that aligned loads and stores will work - const indirectBaseOffset = ((((args_marshal.length * 4) + 7) / 8) | 0) * 8; + try { + argsRoot.value = js_string_to_mono_string(args_marshal); + nameRoot.value = js_string_to_mono_string(friendly_name); + + // Manually assemble an arguments buffer + // (RuntimeTypeHandle, RuntimeMethodHandle, string) + setU32(buffer + 16, typePtr); + setU32(buffer + 32, method); + setU32(buffer + 0, buffer + 16); + setU32(buffer + 4, buffer + 32); + setU32(buffer + 8, argsRoot.value); + setU32(buffer + 12, nameRoot.value); + + // Invoke the managed method + resultRoot.value = cwraps.mono_wasm_invoke_method(generatorMethod, MonoObjectNull, buffer, exceptionRoot.get_address()); + // If it threw an exception, this will yield us a JS Error instance to throw + const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); + if (exc) + throw exc; + // Otherwise it returned a managed String containing the JS for our new function + return conv_string(resultRoot.value); + } finally { + resultRoot.release(); + exceptionRoot.release(); + nameRoot.release(); + argsRoot.release(); + Module._free(buffer); + } +} +function _initialize_bindings_named_closures () : void { + // HACK: Populate the lookup table used by compiled closures const closure: any = { - Module, + _create_temp_frame, + _error: _bindingsError, + _get_args_root_buffer_for_method_call, + _get_buffer_for_method_call, + _handle_exception_for_call, + _js_to_mono_obj, + _js_to_mono_uri, _malloc: Module._malloc, + _pick_automatic_converter_for_type, + _setSpan, + _teardown_after_call, + _unbox_mono_obj_root_with_known_nonprimitive_type, + invoke_method: cwraps.mono_wasm_invoke_method, + js_string_to_mono_string_interned, + js_string_to_mono_string, + js_to_mono_enum, + mono_wasm_new_root, + mono_wasm_try_unbox_primitive_and_get_type: cwraps.mono_wasm_try_unbox_primitive_and_get_type, mono_wasm_unbox_rooted: cwraps.mono_wasm_unbox_rooted, - setI32, - setU32, + getF32, + getF64, + getI32, + getU32, setF32, setF64, - setI64 - }; - let indirectLocalOffset = 0; - - body.push( - "if (!method) throw new Error('no method provided');", - `if (!buffer) buffer = _malloc (${bufferSizeBytes});`, - `let indirectStart = buffer + ${indirectBaseOffset};`, - "" - ); - - for (let i = 0; i < converter.steps.length; i++) { - const step = converter.steps[i]; - const closureKey = "step" + i; - const valueKey = "value" + i; - - const argKey = "arg" + i; - argumentNames.push(argKey); - - if (step.convert) { - closure[closureKey] = step.convert; - body.push(`let ${valueKey} = ${closureKey}(${argKey}, method, ${i});`); - } else { - body.push(`let ${valueKey} = ${argKey};`); - } - - if (step.needs_root) { - body.push("if (!rootBuffer) throw new Error('no root buffer provided');"); - body.push(`rootBuffer.set (${i}, ${valueKey});`); - } - - // HACK: needs_unbox indicates that we were passed a pointer to a managed object, and either - // it was already rooted by our caller or (needs_root = true) by us. Now we can unbox it and - // pass the raw address of its boxed value into the callee. - // FIXME: I don't think this is GC safe - if (step.needs_unbox) - body.push(`${valueKey} = mono_wasm_unbox_rooted (${valueKey});`); - - if (step.indirect) { - const offsetText = `(indirectStart + ${indirectLocalOffset})`; - - switch (step.indirect) { - case "u32": - body.push(`setU32(${offsetText}, ${valueKey});`); - break; - case "i32": - body.push(`setI32(${offsetText}, ${valueKey});`); - break; - case "float": - body.push(`setF32(${offsetText}, ${valueKey});`); - break; - case "double": - body.push(`setF64(${offsetText}, ${valueKey});`); - break; - case "i64": - body.push(`setI64(${offsetText}, ${valueKey});`); - break; - default: - throw new Error("Unimplemented indirect type: " + step.indirect); - } - - body.push(`setU32(buffer + (${i} * 4), ${offsetText});`); - indirectLocalOffset += step.size!; - } else { - body.push(`setI32(buffer + (${i} * 4), ${valueKey});`); - indirectLocalOffset += 4; - } - body.push(""); - } - - body.push("return buffer;"); - - let bodyJs = body.join("\r\n"), compiledFunction = null, compiledVariadicFunction = null; - try { - compiledFunction = _create_named_function("converter_" + converterName, argumentNames, bodyJs, closure); - converter.compiled_function = compiledFunction; - } catch (exc) { - converter.compiled_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); - throw exc; - } - - - argumentNames = ["existingBuffer", "rootBuffer", "method", "args"]; - const variadicClosure = { - converter: compiledFunction + setI32, + setI64, + setU32, }; - body = [ - "return converter(", - " existingBuffer, rootBuffer, method," - ]; - - for (let i = 0; i < converter.steps.length; i++) { - body.push( - " args[" + i + - ( - (i == converter.steps.length - 1) - ? "]" - : "], " - ) - ); - } - - body.push(");"); - - bodyJs = body.join("\r\n"); - try { - compiledVariadicFunction = _create_named_function("variadic_converter_" + converterName, argumentNames, bodyJs, variadicClosure); - converter.compiled_variadic_function = compiledVariadicFunction; - } catch (exc) { - converter.compiled_variadic_function = null; - console.warn("compiling converter failed for", bodyJs, "with error", exc); - throw exc; - } - - converter.scratchRootBuffer = null; - converter.scratchBuffer = VoidPtrNull; - - return converter; + for (const k in closure) + bindings_named_closures.set(k, closure[k]); } -function _maybe_produce_signature_warning(converter: Converter) { - if (converter.has_warned_about_signature) - return; +function _get_api (key: string): Function { + if (!bindings_named_closures_initialized) { + bindings_named_closures_initialized = true; + _initialize_bindings_named_closures(); + } - console.warn("MONO_WASM: Deprecated raw return value signature: '" + converter.args_marshal + "'. End the signature with '!' instead of 'm'."); - converter.has_warned_about_signature = true; + const result = bindings_named_closures.get(key); + if (!result || typeof(result) !== "function") + throw new Error(`Expected ${key} to be a function but was '${result}'`); + return result; } -export function _decide_if_result_is_marshaled(converter: Converter, argc: number): boolean { - if (!converter) - return true; - - if ( - converter.is_result_possibly_unmarshaled && - (argc === converter.result_unmarshaled_if_argc) - ) { - if (argc < converter.result_unmarshaled_if_argc) - throw new Error(`Expected >= ${converter.result_unmarshaled_if_argc} argument(s) but got ${argc} for signature '${converter.args_marshal}'`); - - _maybe_produce_signature_warning(converter); - return false; - } else { - if (argc < converter.steps.length) - throw new Error(`Expected ${converter.steps.length} argument(s) but got ${argc} for signature '${converter.args_marshal}'`); +export function _compile_converter_for_marshal_string(typePtr: MonoType, method: MonoMethod, args_marshal: ArgsMarshalString): SignatureConverter { + const converter = _get_converter_for_marshal_string(typePtr, method, args_marshal); + if (converter && converter.compiled_function && converter.compiled_variadic_function) + return converter; - return !converter.is_result_definitely_unmarshaled; - } + let csFuncResult : any = null; + // HACK: We invoke this method directly instead of using the cswraps. version, since that wrapper relies on this function + const js = _generate_args_marshaler(typePtr, method, args_marshal); + const csFunc = new Function("get_api", "get_type_converter", js); + csFuncResult = csFunc(_get_api, _pick_automatic_converter_for_type); + return csFuncResult; } export function mono_bind_method(method: MonoMethod, this_arg: MonoObject | null, args_marshal: ArgsMarshalString, friendly_name: string): Function { @@ -377,176 +307,64 @@ export function mono_bind_method(method: MonoMethod, this_arg: MonoObject | null 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); + // 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 (!this_arg) { + 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, + // We shove this_arg into a root ASAP since our invokes below could cause + // a GC to occur and move the object + thisArgRoot: (this_arg | 0) !== 0 ? mono_wasm_new_root(this_arg) : null, 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;"); - } + let typePtr : MonoType = MonoTypeNull; - if (converter && converter.is_result_definitely_unmarshaled) { - body.push("let is_result_marshaled = false;"); - } else if (converter && converter.is_result_possibly_unmarshaled) { - body.push(`let is_result_marshaled = arguments.length !== ${converter.result_unmarshaled_if_argc};`); - } else { - body.push("let is_result_marshaled = true;"); - } - - // We inline a bunch of the invoke and marshaling logic here in order to eliminate the GC pressure normally - // created by the unboxing part of the call process. Because unbox_mono_obj(_root) can return non-numeric - // types, v8 and spidermonkey allocate and store its result on the heap (in the nursery, to be fair). - // For a bound method however, we know the result will always be the same type because C# methods have known - // return types. Inlining the invoke and marshaling logic means that even though the bound method has logic - // for handling various types, only one path through the method (for its appropriate return type) will ever - // be taken, and the JIT will see that the 'result' local and thus the return value of this function are - // always of the exact same type. All of the branches related to this end up being predicted and low-cost. - // The end result is that bound method invocations don't always allocate, so no more nursery GCs. Yay! -kg - body.push( - "", - "resultRoot.value = invoke_method (method, this_arg, buffer, exceptionRoot.get_address ());", - `_handle_exception_for_call (${converterKey}, token, buffer, resultRoot, exceptionRoot, argsRootBuffer);`, - "", - "let resultPtr = resultRoot.value, result = undefined;" - ); - - if (converter) { - if (converter.is_result_possibly_unmarshaled) - body.push("if (!is_result_marshaled) "); - - if (converter.is_result_definitely_unmarshaled || converter.is_result_possibly_unmarshaled) - body.push(" result = resultPtr;"); - - if (!converter.is_result_definitely_unmarshaled) - body.push( - "if (is_result_marshaled && (resultPtr !== 0)) {", - // For the common scenario where the return type is a primitive, we want to try and unbox it directly - // into our existing heap allocation and then read it out of the heap. Doing this all in one operation - // means that we only need to enter a gc safe region twice (instead of 3+ times with the normal, - // slower check-type-and-then-unbox flow which has extra checks since unbox verifies the type). - " let resultType = mono_wasm_try_unbox_primitive_and_get_type (resultPtr, unbox_buffer, unbox_buffer_size);", - " switch (resultType) {", - ` case ${MarshalType.INT}:`, - " result = getI32(unbox_buffer); break;", - ` case ${MarshalType.POINTER}:`, // FIXME: Is this right? - ` case ${MarshalType.UINT32}:`, - " result = getU32(unbox_buffer); break;", - ` case ${MarshalType.FP32}:`, - " result = getF32(unbox_buffer); break;", - ` case ${MarshalType.FP64}:`, - " result = getF64(unbox_buffer); break;", - ` case ${MarshalType.BOOL}:`, - " result = getI32(unbox_buffer) !== 0; break;", - ` case ${MarshalType.CHAR}:`, - " result = String.fromCharCode(getI32(unbox_buffer)); break;", - " default:", - " result = _unbox_mono_obj_root_with_known_nonprimitive_type (resultRoot, resultType, unbox_buffer); break;", - " }", - "}" - ); - } else { - throw new Error("No converter"); + let converter: SignatureConverter | null = null; + if (typeof (args_marshal) === "string") { + const classPtr = cwraps.mono_wasm_get_class_for_bind_or_invoke(this_arg, method); + if (!classPtr) + throw new Error(`Could not get class ptr for bind_method with this (${this_arg}) and 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); + // HACK: If the bound method has a this-arg, we don't want to store it into the cache + // since this indicates that the caller may be binding lots of methods onto instances + if (!this_arg) + _bound_method_cache.set(cacheKey, result); return result; } -declare const enum ArgsMarshal { +export enum ArgsMarshal { Int32 = "i", // int32 Int32Enum = "j", // int32 - Enum with underlying type of int32 Int64 = "l", // int64 @@ -554,59 +372,47 @@ 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 ArgsMarshalString = string; + +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 = "!" | ""; - -// TODO make this more efficient so we can add more parameters (currently it only checks up to 4). One option is to add a -// blank to the ArgsMarshal enum but that doesn't solve the TS limit of number of options in 1 type -// Take the marshaling enums and convert to all the valid strings for type checking. -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?: ArgsMarshalString; 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; + thisArgRoot: WasmRoot | null; 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 64d4d06eb8dcb5..1909cdac4b9203 100644 --- a/src/mono/wasm/runtime/method-calls.ts +++ b/src/mono/wasm/runtime/method-calls.ts @@ -13,9 +13,9 @@ 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 { ArgsMarshalString, mono_bind_method, - Converter, _compile_converter_for_marshal_string, - _decide_if_result_is_marshaled, find_method, - BoundMethodToken + 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 +23,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: ArgsMarshalString, args: any) { +function _verify_args_for_method_call(args_marshal: ArgsMarshalString, args: any) : boolean { const has_args = args && (typeof args === "object") && args.length > 0; const has_args_marshal = typeof args_marshal === "string"; @@ -37,7 +37,7 @@ function _verify_args_for_method_call(args_marshal: ArgsMarshalString, args: any 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 +52,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 +73,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 +82,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 +100,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 +113,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; @@ -160,9 +160,14 @@ export function call_method(method: MonoMethod, this_arg: MonoObject | undefined // check if the method signature needs argument mashalling if (needs_converter) { - converter = _compile_converter_for_marshal_string(args_marshal); + const classPtr = cwraps.mono_wasm_get_class_for_bind_or_invoke(this_arg, method); + if (!classPtr) + throw new Error (`Could not get class ptr for call_method with this (${this_arg}) and method (${method})`); - is_result_marshaled = _decide_if_result_is_marshaled(converter, args.length); + const typePtr = cwraps.mono_wasm_class_get_type(classPtr); + converter = _compile_converter_for_marshal_string(typePtr, method, args_marshal); + + is_result_marshaled = !converter.is_result_definitely_unmarshaled; argsRootBuffer = _get_args_root_buffer_for_method_call(converter, null); @@ -175,11 +180,11 @@ export function call_method(method: MonoMethod, this_arg: MonoObject | undefined export function _handle_exception_for_call( - converter: Converter | undefined, token: BoundMethodToken | null, - buffer: VoidPtr, resultRoot: WasmRoot, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, + buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer?: WasmRootBuffer ): void { - const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); + const exc = _convert_exception_for_method_call(resultRoot.value, exceptionRoot.value); if (!exc) return; @@ -188,8 +193,8 @@ export function _handle_exception_for_call( } function _handle_exception_and_produce_result_for_call( - converter: Converter | undefined, token: BoundMethodToken | null, - buffer: VoidPtr, resultRoot: WasmRoot, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, + buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer: WasmRootBuffer | undefined, is_result_marshaled: boolean ): any { @@ -205,7 +210,7 @@ function _handle_exception_and_produce_result_for_call( } export function _teardown_after_call( - converter: Converter | undefined, token: BoundMethodToken | null, + converter: SignatureConverter | undefined, token: BoundMethodToken | null, buffer: VoidPtr, resultRoot: WasmRoot, exceptionRoot: WasmRoot, argsRootBuffer?: WasmRootBuffer ): void { @@ -230,12 +235,12 @@ 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); } diff --git a/src/mono/wasm/runtime/roots.ts b/src/mono/wasm/runtime/roots.ts index 010732c23a8940..463f1e136e99e4 100644 --- a/src/mono/wasm/runtime/roots.ts +++ b/src/mono/wasm/runtime/roots.ts @@ -10,6 +10,7 @@ let _scratch_root_buffer: WasmRootBuffer | null = null; let _scratch_root_free_indices: Int32Array | null = null; let _scratch_root_free_indices_count = 0; const _scratch_root_free_instances: WasmRoot[] = []; +const _external_root_free_instances: WasmExternalRoot[] = []; /** * Allocates a block of memory that can safely contain pointers into the managed heap. @@ -52,6 +53,26 @@ export function mono_wasm_new_root_buffer_from_pointer(offset: VoidPtr, capacity return new WasmRootBuffer(offset, capacity, false, name); } +/** + * Allocates a WasmRoot pointing to a root provided and controlled by external code. + * Releasing this root will not do anything, but you still need to call .release(). + */ +export function mono_wasm_new_external_root(address: VoidPtr): WasmExternalRoot { + let result: WasmExternalRoot; + + if (!address) + throw new Error("address must be a location in the native heap"); + + if (_external_root_free_instances.length > 0) { + result = _external_root_free_instances.pop()!; + result._set_address(address); + } else { + result = new WasmExternalRoot(address); + } + + return result; +} + /** * Allocates temporary storage for a pointer into the managed heap. * Pointers stored here will be visible to the GC, ensuring that the object they point to aren't moved or collected. @@ -301,3 +322,56 @@ export class WasmRoot { return `[root @${this.get_address()}]`; } } + +export class WasmExternalRoot extends WasmRoot { + // @ts-ignore + private __external_address: NativePointer; + // @ts-ignore + private __external_address_32: number; + + constructor (address : NativePointer) { + super(null, 0); + this._set_address(address); + } + + _set_address(address : NativePointer) { + this.__external_address = address; + this.__external_address_32 = address >>> 2; + } + + get_address(): NativePointer { + return this.__external_address; + } + + get_address_32(): number { + return this.__external_address_32; + } + + get(): T { + const result = Module.HEAPU32[this.__external_address_32]; + return result; + } + + set(value: T): T { + Module.HEAPU32[this.__external_address_32] = value; + return value; + } + + get value(): T { + return this.get(); + } + + set value(value: T) { + this.set(value); + } + + release(): void { + const maxPooledInstances = 128; + if (_external_root_free_instances.length < maxPooledInstances) + _external_root_free_instances.push(this); + } + + toString(): string { + return `[external root @${this.get_address()}]`; + } +} \ No newline at end of file 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 f592f488327cf6..5e341bf8dabe50 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(); } @@ -273,6 +286,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;