diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/Directory.Build.props b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/Directory.Build.props new file mode 100644 index 00000000000000..f48a524d4784ee --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/Directory.Build.props @@ -0,0 +1,6 @@ + + + true + + + \ No newline at end of file diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/CommonJSMethodGenerator.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/CommonJSMethodGenerator.cs new file mode 100644 index 00000000000000..98a9e3a4117b65 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/CommonJSMethodGenerator.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + + +namespace JavaScript.MarshalerGenerator +{ + internal class CommonJSMethodGenerator + { + public StringBuilder prolog; + public MethodDeclarationSyntax MethodSyntax; + public TypeDeclarationSyntax TypeSyntax; + public AttributeSyntax AttributeSyntax; + public IMethodSymbol MethodSymbol; + public IMethodSymbol AttributeSymbol; + public AttributeData JSAttributeData; + public JSMarshalerSig[] ParemeterSignatures; + public JSMarshalerSig ReturnSignature; + public MarshalerSelector MarshalerSelector; + public string BoundFunctionName; + public string AssemblyName; + + public ITypeSymbol ReturnType => MethodSymbol.ReturnType; + public TypeSyntax ReturnTypeSyntax => ReturnType.AsTypeSyntax(); + public string MethodName => MethodSymbol.Name; + public bool HasCustomMarshalers => JSAttributeData.ConstructorArguments.Length > 1; + public bool IsVoidMethod => ReturnType.SpecialType == SpecialType.System_Void; + + public void SelectMarshalers(Compilation compilation) + { + JSMarshalerMetadata[] customMarshalers = null; + if (HasCustomMarshalers) + { + ImmutableArray marshalerTypes = JSAttributeData.ConstructorArguments[1].Values; + customMarshalers = marshalerTypes.Select(mt => ExtractMarshalerMeta(compilation, mt)).ToArray(); + } + + MarshalerSelector = new MarshalerSelector(compilation); + ReturnSignature = MarshalerSelector.GetArgumentSignature(prolog, customMarshalers, MethodSymbol.ReturnType); + for (int i = 0; i < MethodSymbol.Parameters.Length; i++) + { + IParameterSymbol arg = MethodSymbol.Parameters[i]; + ParemeterSignatures[i] = MarshalerSelector.GetArgumentSignature(prolog, customMarshalers, arg.Type); + } + AssemblyName = compilation.AssemblyName; + } + + protected ArgumentSyntax CreateMarshallersSyntax() + { + ArgumentSyntax marshallersArg; + List marshalersTypes = HasCustomMarshalers + ? JSAttributeData.ConstructorArguments[1].Values.Select(a => (ITypeSymbol)a.Value).ToList() + : new List(); + + if (ReturnSignature.IsAuto) + { + marshalersTypes.Add(ReturnSignature.MarshalerType); + } + marshalersTypes.AddRange(ParemeterSignatures.Where(s => s.IsAuto).Select(s => s.MarshalerType)); + + if (marshalersTypes.Count > 0) + { + var marshalerInstances = marshalersTypes.Distinct(SymbolEqualityComparer.Default).Cast().Select(t => + { + return ObjectCreationExpression(t.AsTypeSyntax()).WithArgumentList(ArgumentList()); + }); + marshallersArg = Argument(ImplicitArrayCreationExpression(InitializerExpression(SyntaxKind.ArrayInitializerExpression, SeparatedList(marshalerInstances)))); + } + else + { + marshallersArg = Argument(LiteralExpression(SyntaxKind.NullLiteralExpression)); + } + + return marshallersArg; + } + + protected static TypeDeclarationSyntax CreateTypeDeclarationWithoutTrivia(TypeDeclarationSyntax typeDeclaration) + { + var mods = AddToModifiers(StripTriviaFromModifiers(typeDeclaration.Modifiers), SyntaxKind.UnsafeKeyword); + return TypeDeclaration(typeDeclaration.Kind(), typeDeclaration.Identifier) + .WithModifiers(mods); + } + + protected static SyntaxTokenList AddToModifiers(SyntaxTokenList modifiers, SyntaxKind modifierToAdd) + { + if (modifiers.IndexOf(modifierToAdd) >= 0) + return modifiers; + + int idx = modifiers.IndexOf(SyntaxKind.PartialKeyword); + return idx >= 0 + ? modifiers.Insert(idx, Token(modifierToAdd)) + : modifiers.Add(Token(modifierToAdd)); + } + + protected static SyntaxTokenList StripTriviaFromModifiers(SyntaxTokenList tokenList) + { + SyntaxToken[] strippedTokens = new SyntaxToken[tokenList.Count]; + for (int i = 0; i < tokenList.Count; i++) + { + strippedTokens[i] = tokenList[i].WithoutTrivia(); + } + return new SyntaxTokenList(strippedTokens); + } + + protected JSMarshalerMetadata ExtractMarshalerMeta(Compilation compilation, TypedConstant mt) + { + try + { + INamedTypeSymbol? marshalerType = compilation.GetTypeByMetadataName(mt.Value.ToString()); + ITypeSymbol marshaledType = marshalerType.BaseType.TypeArguments[0]; + + var hasAfterJs = marshalerType.GetMembers("AfterToJavaScript").Length > 0; + + return new JSMarshalerMetadata + { + MarshalerType = marshalerType, + MarshaledType = marshaledType, + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + AfterToJsMethod = hasAfterJs ? "AfterMarshalToJs" : null, + }; + } + catch (Exception ex) + { + prolog.AppendLine($"Failed when processing {mt.Value} \n" + ex.Message); + return null; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/Constants.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/Constants.cs new file mode 100644 index 00000000000000..56f17e4948af76 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/Constants.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +namespace JavaScript.MarshalerGenerator +{ + internal static class Constants + { + public const int JavaScriptMarshalerArgSize = 16; + public const string JavaScriptMarshal = "System.Runtime.InteropServices.JavaScript.JavaScriptMarshal"; + public const string JavaScriptPublic = "System.Runtime.InteropServices.JavaScript"; + + public const string JavaScriptMarshalGlobal = "global::" + JavaScriptMarshal; + public const string JavaScriptMarshalerSignatureGlobal = "global::System.Runtime.InteropServices.JavaScript.JavaScriptMarshalerSignature"; + public const string ModuleInitializerAttributeGlobal = "global::System.Runtime.CompilerServices.ModuleInitializerAttribute"; + public const string DynamicDependencyAttributeGlobal = "global::System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute"; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportGenerator.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportGenerator.cs new file mode 100644 index 00000000000000..6d6febcfc9317a --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportGenerator.cs @@ -0,0 +1,112 @@ +// 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.Immutable; +using System.Linq; +using System.Text; +using JavaScript.MarshalerGenerator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace System.Runtime.InteropServices.JavaScript +{ + [Generator] + internal class JSExportGenerator : IIncrementalGenerator + { + private const string AttributeFullName = "System.Runtime.InteropServices.JavaScript.JSExportAttribute"; + private const string Category = "JSExport"; + private const string Prefix = "JSExport"; +#pragma warning disable RS2008 //TODO remove this + public static DiagnosticDescriptor RequireStaticDD = new DiagnosticDescriptor(Prefix + "002", "JSExportAttribute requires static method", "JSExportAttribute requires static method", Category, DiagnosticSeverity.Error, true); + public static void Debug(SourceProductionContext context, string message) + { + var dd = new DiagnosticDescriptor(Prefix + "000", message, message, Category, DiagnosticSeverity.Warning, true); + context.ReportDiagnostic(Diagnostic.Create(dd, Location.None)); + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider methodDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => IsMethodDeclarationWithAnyAttribute(s), + static (ctx, _) => GetMethodDeclarationsWithMarshalerAttribute(ctx) + ) + .Where(static m => m is not null); + + IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndClasses = context.CompilationProvider.Combine(methodDeclarations.Collect()); + + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item1, source.Item2, spc)); + } + + private static bool IsMethodDeclarationWithAnyAttribute(SyntaxNode node) + => node is MethodDeclarationSyntax m && m.AttributeLists.Count > 0; + + private static JSExportMethodGenerator GetMethodDeclarationsWithMarshalerAttribute(GeneratorSyntaxContext context) + { + var methodSyntax = (MethodDeclarationSyntax)context.Node; + + foreach (AttributeListSyntax attributeListSyntax in methodSyntax.AttributeLists) + { + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + IMethodSymbol attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; + if (attributeSymbol != null) + { + string fullName = attributeSymbol.ContainingType.ToDisplayString(); + if (fullName == AttributeFullName) + { + IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax); + var attributeData = methodSymbol.GetAttributes(); + AttributeData JSExportData = attributeData.Where(d => d.AttributeClass.ToDisplayString() == AttributeFullName).Single(); + + + var methodGenrator = new JSExportMethodGenerator(methodSyntax, attributeSyntax, methodSymbol, attributeSymbol, JSExportData); + + return methodGenrator; + } + } + } + } + + return null; + } + + private static void Execute(Compilation compilation, ImmutableArray methods, SourceProductionContext context) + { + if (methods.IsDefaultOrEmpty) + return; + + var fileText = new StringBuilder(); + foreach (JSExportMethodGenerator method in methods) + { + if (!method.MethodSymbol.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create(RequireStaticDD, method.MethodSyntax.GetLocation())); + continue; + } + try + { + method.SelectMarshalers(compilation); + + string code = method.GenerateWrapper(); + // this is just for debug + fileText.AppendLine("/* " + method.MethodName + " " + DateTime.Now.ToString("o")); + fileText.Append(method.prolog.ToString()); + fileText.AppendLine("*/\n"); + fileText.AppendLine(code); + } + catch (Exception ex) + { + // this is just for debug + fileText.AppendLine("/* " + method.MethodName + " " + DateTime.Now.ToString("o")); + fileText.AppendLine(method.MethodSyntax.ToString()); + fileText.Append(method.prolog.ToString()); + fileText.AppendLine(ex.ToString()); + fileText.AppendLine("*/"); + } + } + context.AddSource("JSExport.g.cs", fileText.ToString()); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportMethodGenerator.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportMethodGenerator.cs new file mode 100644 index 00000000000000..c1536cd6d513c8 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSExportMethodGenerator.cs @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace JavaScript.MarshalerGenerator +{ + internal sealed class JSExportMethodGenerator : CommonJSMethodGenerator + { + public string WrapperName; + public string RegistrationName; + public int Hash; + + public JSExportMethodGenerator(MethodDeclarationSyntax methodSyntax, + AttributeSyntax attributeSyntax, + IMethodSymbol methodSymbol, + IMethodSymbol attributeSymbol, + AttributeData jsAttrData) + { + MethodSyntax = methodSyntax; + MethodSymbol = methodSymbol; + + AttributeSymbol = attributeSymbol; + AttributeSyntax = attributeSyntax; + JSAttributeData = jsAttrData; + BoundFunctionName = jsAttrData.ConstructorArguments.Length > 0 + ? jsAttrData.ConstructorArguments[0].Value.ToString() + : null; + TypeSyntax = methodSyntax.Parent as TypeDeclarationSyntax; + ParemeterSignatures = new JSMarshalerSig[MethodSymbol.Parameters.Length]; + prolog = new StringBuilder(); + + int hash = 17; + unchecked + { + foreach (var param in MethodSymbol.Parameters) + { + hash = hash * 31 + param.Type.Name.GetHashCode(); + } + } + Hash = Math.Abs(hash); + WrapperName = "__Wrapper_" + MethodName + "_" + Hash; + RegistrationName = "__Register_" + MethodName + "_" + Hash; + } + + public string GenerateWrapper() + { + NamespaceDeclarationSyntax namespaceSyntax = MethodSymbol.ContainingType.ContainingNamespace.AsNamespace(); + TypeDeclarationSyntax typeSyntax = CreateTypeDeclarationWithoutTrivia(TypeSyntax); + + IEnumerable wrapperStatements = WrapperSyntax(); + IEnumerable registerStatements = RegistrationSyntax(); + + MemberDeclarationSyntax wrappperMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier(WrapperName)) + .WithModifiers(TokenList(new[] { Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword) })) + .WithParameterList(ParameterList(SingletonSeparatedList( + Parameter(Identifier("buffer")).WithType(PointerType(PredefinedType(Token(SyntaxKind.VoidKeyword))))))) + .WithBody(Block(List(wrapperStatements))); + + MemberDeclarationSyntax registerMethod = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier(RegistrationName)) + .WithAttributeLists(List(new AttributeListSyntax[]{ + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(Constants.ModuleInitializerAttributeGlobal)))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(Constants.DynamicDependencyAttributeGlobal)) + .WithArgumentList(AttributeArgumentList(SeparatedList(new[]{ + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(WrapperName))), + AttributeArgument(TypeOfExpression(MethodSymbol.ContainingType.AsTypeSyntax()))} + )))))})) + .WithModifiers(TokenList(new[] { Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword) })) + .WithBody(Block(registerStatements)); + + CompilationUnitSyntax syntax = CompilationUnit() + .WithMembers(SingletonList(namespaceSyntax + .WithMembers(SingletonList(typeSyntax + .WithMembers(List(new[] { wrappperMethod, registerMethod }) + ))))); + + return syntax.NormalizeWhitespace().ToFullString(); + } + + private IEnumerable RegistrationSyntax() + { + var fullyQualifiedName = $"[{AssemblyName}]{MethodSymbol.ContainingType.ToDisplayString()}:{MethodName}"; + var signatureArgs = new List(); + signatureArgs.Add(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(fullyQualifiedName)))); + signatureArgs.Add(Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(Hash)))); + signatureArgs.Add(Argument(BoundFunctionName != null + ? LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(BoundFunctionName)) + : LiteralExpression(SyntaxKind.NullLiteralExpression))); + signatureArgs.Add(CreateMarshallersSyntax()); + signatureArgs.Add(Argument(TypeOfExpression(ReturnType.AsTypeSyntax()))); + signatureArgs.AddRange(MethodSymbol.Parameters.Select(p => Argument(TypeOfExpression(p.Type.AsTypeSyntax())))); + + yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshal), IdentifierName("BindCSFunction"))) + .WithArgumentList(ArgumentList(SeparatedList(signatureArgs)))); + } + + public IEnumerable WrapperSyntax() + { + yield return LocalDeclarationStatement(VariableDeclaration( + IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList()))) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier("__args")) + .WithInitializer(EqualsValueClause(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshal), IdentifierName("CreateArguments"))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(IdentifierName("buffer")))))))))); + + var statements=new List(); + var arguments=new List(); + + for (int i = 0; i < MethodSymbol.Parameters.Length; i++) + { + IParameterSymbol arg = MethodSymbol.Parameters[i]; + ExpressionSyntax invocation = InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + ParemeterSignatures[i].MarshalerType.AsTypeSyntax(), IdentifierName(ParemeterSignatures[i].ToManagedMethod))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(ElementAccessExpression(IdentifierName("__args")) + .WithArgumentList(BracketedArgumentList(SingletonSeparatedList( + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i + 1)))))))))); + + if (ParemeterSignatures[i].NeedsCast) + { + invocation = CastExpression(MethodSymbol.Parameters[i].Type.AsTypeSyntax(), invocation); + } + + statements.Add(LocalDeclarationStatement(VariableDeclaration( + IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList()))) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier(arg.Name)) + .WithInitializer(EqualsValueClause(invocation)))))); + + arguments.Add(Argument(IdentifierName(arg.Name))); + } + if (IsVoidMethod) + { + statements.Add(ExpressionStatement(InvocationExpression(IdentifierName(MethodName)) + .WithArgumentList(ArgumentList(SeparatedList(arguments))))); + } + else + { + ExpressionSyntax invocation = InvocationExpression(IdentifierName(MethodName)) + .WithArgumentList(ArgumentList(SeparatedList(arguments))); + if (ReturnSignature.NeedsCast) + { + invocation = CastExpression(ReturnSignature.MarshaledType.AsTypeSyntax(), invocation); + } + statements.Add(LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList()))) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier("__res")) + .WithInitializer(EqualsValueClause(invocation)))))); + + statements.Add(ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + ReturnSignature.MarshalerType.AsTypeSyntax(), IdentifierName(ReturnSignature.ToJsMethod))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName("__res")).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("__args"), IdentifierName("Result")))}))))); + } + + yield return TryStatement(SingletonList(CatchClause() + .WithDeclaration(CatchDeclaration(IdentifierName("Exception")).WithIdentifier(Identifier("__ex"))) + .WithBlock(Block(SingletonList( + ExpressionStatement(InvocationExpression( + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshal), IdentifierName("MarshalExceptionToJs"))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName("__ex")).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("__args"), IdentifierName("Exception")))}))))))))) + .WithBlock(Block(statements)); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportGenerator.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportGenerator.cs new file mode 100644 index 00000000000000..57c490733b44ba --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportGenerator.cs @@ -0,0 +1,118 @@ +// 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.Immutable; +using System.Linq; +using System.Text; +using JavaScript.MarshalerGenerator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace System.Runtime.InteropServices.JavaScript +{ + [Generator] + internal class JsImportGenerator : IIncrementalGenerator + { + private const string AttributeFullName = "System.Runtime.InteropServices.JavaScript.JSImportAttribute"; + private const string Category = "JsImport"; + private const string Prefix = "JsImport"; +#pragma warning disable RS2008 //TODO remove this + public static DiagnosticDescriptor RequirePartialDD = new DiagnosticDescriptor(Prefix + "001", "JSImportAttribute requires partial method", "JSImportAttribute requires partial method", Category, DiagnosticSeverity.Error, true); + public static DiagnosticDescriptor RequireStaticDD = new DiagnosticDescriptor(Prefix + "002", "JSImportAttribute requires static method", "JSImportAttribute requires static method", Category, DiagnosticSeverity.Error, true); + public static void Debug(SourceProductionContext context, string message) + { + var dd = new DiagnosticDescriptor(Prefix + "000", message, message, Category, DiagnosticSeverity.Warning, true); + context.ReportDiagnostic(Diagnostic.Create(dd, Location.None)); + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider methodDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => IsMethodDeclarationWithAnyAttribute(s), + static (ctx, _) => GetMethodDeclarationsWithMarshalerAttribute(ctx) + ) + .Where(static m => m is not null); + + IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndClasses = context.CompilationProvider.Combine(methodDeclarations.Collect()); + + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item1, source.Item2, spc)); + } + + private static bool IsMethodDeclarationWithAnyAttribute(SyntaxNode node) + => node is MethodDeclarationSyntax m && m.AttributeLists.Count > 0; + + private static JSImportMethodGenerator GetMethodDeclarationsWithMarshalerAttribute(GeneratorSyntaxContext context) + { + var methodSyntax = (MethodDeclarationSyntax)context.Node; + + foreach (AttributeListSyntax attributeListSyntax in methodSyntax.AttributeLists) + { + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + IMethodSymbol attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; + if (attributeSymbol != null) + { + string fullName = attributeSymbol.ContainingType.ToDisplayString(); + if (fullName == AttributeFullName) + { + IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax); + var attributeData = methodSymbol.GetAttributes(); + AttributeData jsImportData = attributeData.Where(d => d.AttributeClass.ToDisplayString() == AttributeFullName).Single(); + + + var methodGenrator = new JSImportMethodGenerator(methodSyntax, attributeSyntax, methodSymbol, attributeSymbol, jsImportData); + + return methodGenrator; + } + } + } + } + + return null; + } + + private static void Execute(Compilation compilation, ImmutableArray methods, SourceProductionContext context) + { + if (methods.IsDefaultOrEmpty) + return; + + var fileText = new StringBuilder(); + foreach (JSImportMethodGenerator method in methods) + { + if (!method.MethodSymbol.IsPartialDefinition) + { + context.ReportDiagnostic(Diagnostic.Create(RequirePartialDD, method.MethodSyntax.GetLocation())); + continue; + } + if (!method.MethodSymbol.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create(RequireStaticDD, method.MethodSyntax.GetLocation())); + continue; + } + try + { + method.SelectMarshalers(compilation); + + string code = method.GenerateWrapper(); + // this is just for debug + fileText.AppendLine("/* " + method.MethodName + " " + DateTime.Now.ToString("o")); + fileText.Append(method.prolog.ToString()); + fileText.AppendLine("*/\n"); + fileText.AppendLine(code); + } + catch (Exception ex) + { + // this is just for debug + fileText.AppendLine("/* " + method.MethodName + " " + DateTime.Now.ToString("o")); + fileText.AppendLine(method.MethodSyntax.ToString()); + fileText.Append(method.prolog.ToString()); + fileText.AppendLine(ex.ToString()); + fileText.AppendLine("*/"); + } + } + context.AddSource("JsImport.g.cs", fileText.ToString()); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportMethodGenerator.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportMethodGenerator.cs new file mode 100644 index 00000000000000..91b2a881b4435b --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSImportMethodGenerator.cs @@ -0,0 +1,229 @@ +// 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.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace JavaScript.MarshalerGenerator +{ + internal sealed class JSImportMethodGenerator : CommonJSMethodGenerator + { + public string BindingName; + + public JSImportMethodGenerator(MethodDeclarationSyntax methodSyntax, + AttributeSyntax attributeSyntax, + IMethodSymbol methodSymbol, + IMethodSymbol attributeSymbol, + AttributeData jsAttrData) + { + MethodSyntax = methodSyntax; + MethodSymbol = methodSymbol; + AttributeSymbol = attributeSymbol; + AttributeSyntax = attributeSyntax; + JSAttributeData = jsAttrData; + BoundFunctionName = jsAttrData.ConstructorArguments[0].Value.ToString(); + TypeSyntax = methodSyntax.Parent as TypeDeclarationSyntax; + ParemeterSignatures = new JSMarshalerSig[MethodSymbol.Parameters.Length]; + prolog = new StringBuilder(); + + uint hash = 17; + unchecked + { + foreach (var param in MethodSymbol.Parameters) + { + hash = hash * 31 + (uint)param.Type.Name.GetHashCode(); + } + } + BindingName = "__Binding_" + MethodName + "_" + hash; + } + + + public string GenerateWrapper() + { + NamespaceDeclarationSyntax namespaceSyntax = MethodSymbol.ContainingType.ContainingNamespace.AsNamespace(); + TypeDeclarationSyntax typeSyntax = CreateTypeDeclarationWithoutTrivia(TypeSyntax); + + IEnumerable parametersWithTypes = MethodSymbol.Parameters.Select(p => Parameter(Identifier(p.Name)).WithType(p.Type.AsTypeSyntax())); + IEnumerable statements = new[] { BindSyntax() } + .Union(AllocationSyntax()) + .Union(InitSyntax()) + .Union(ConvertSyntax()) + .Union(CallSyntax()) + .Union(AfterSyntax()) + .Union(ReturnSyntax()) + ; + MemberDeclarationSyntax wrappperMethod = MethodDeclaration(ReturnTypeSyntax, Identifier(MethodName)) + .WithModifiers(MethodSyntax.Modifiers) + .WithParameterList(ParameterList(SeparatedList(parametersWithTypes))) + .WithBody(Block(List(statements))); + MemberDeclarationSyntax bindingField = BindingField(); + + CompilationUnitSyntax syntax = CompilationUnit() + .WithMembers(SingletonList(namespaceSyntax + .WithMembers(SingletonList(typeSyntax + .WithMembers(List(new[] { bindingField, wrappperMethod }) + ))))); + + return syntax.NormalizeWhitespace().ToFullString(); + } + + private MemberDeclarationSyntax BindingField() + { + return FieldDeclaration(VariableDeclaration(IdentifierName(Constants.JavaScriptMarshalerSignatureGlobal)) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier(BindingName))))) + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + ; + } + + private StatementSyntax BindSyntax() + { + + + var bindingParameters = + (new ArgumentSyntax[] { + // name + Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(BoundFunctionName))), + // marshallers + CreateMarshallersSyntax(), + // return type + Argument(TypeOfExpression(ReturnType.AsTypeSyntax())), + }) + // parameter types + .Union(MethodSymbol.Parameters.Select(p => Argument(TypeOfExpression(p.Type.AsTypeSyntax())))); + + return IfStatement(BinaryExpression(SyntaxKind.EqualsExpression, IdentifierName(BindingName), LiteralExpression(SyntaxKind.NullLiteralExpression)), + Block(SingletonList( + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(BindingName), + InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("BindJSFunction"))) + .WithArgumentList(ArgumentList(SeparatedList(bindingParameters)))))))); + } + + private IEnumerable AllocationSyntax() + { + yield return LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier("var"))) + .WithVariables(SeparatedList(new[]{VariableDeclarator(Identifier("__excMessage")) + .WithInitializer(EqualsValueClause(DefaultExpression(PredefinedType(Token(SyntaxKind.StringKeyword))))) }))); + if (!IsVoidMethod) yield return LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier("var"))) + .WithVariables(SeparatedList(new[]{VariableDeclarator(Identifier("__resRoot")) + .WithInitializer(EqualsValueClause(DefaultExpression(PredefinedType(Token(SyntaxKind.ObjectKeyword))))) }))); + yield return LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier("var"))) + .WithVariables(SeparatedList(new[]{VariableDeclarator(Identifier("__buffer")) + .WithInitializer(EqualsValueClause( + StackAllocArrayCreationExpression(ArrayType(PredefinedType(Token(SyntaxKind.ByteKeyword))) + .WithRankSpecifiers(SingletonList(ArrayRankSpecifier(SingletonSeparatedList( + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(BindingName), IdentifierName("TotalBufferLength"))))))))) }))); + yield return LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier("var"))) + .WithVariables(SeparatedList(new[]{VariableDeclarator(Identifier("__args")) + .WithInitializer(EqualsValueClause(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("CreateArguments"))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(IdentifierName("__buffer"))))))) }))); + for (int i = 0; i < ParemeterSignatures.Length; i++) + { + if (ParemeterSignatures[i].NeedsCast) + { + yield return LocalDeclarationStatement(VariableDeclaration(IdentifierName(Identifier("var"))) + .WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier("___"+MethodSymbol.Parameters[i].Name)) + .WithInitializer(EqualsValueClause(CastExpression(ParemeterSignatures[i].MarshaledType.AsTypeSyntax(), IdentifierName(MethodSymbol.Parameters[i].Name))))))); + } + } + } + + private IEnumerable InitSyntax() + { + if (IsVoidMethod) yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("InitVoid"))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName("__excMessage")).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("__args"))})))); + + else yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("InitResult"))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName("__excMessage")).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("__resRoot")).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("__args")), + Argument(IdentifierName(BindingName))})))); + + for (int i = 0; i < MethodSymbol.Parameters.Length; i++) + { + IParameterSymbol arg = MethodSymbol.Parameters[i]; + yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("InitArgument"))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i+1))), + Argument(IdentifierName(arg.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("__args")), + Argument(IdentifierName(BindingName))})))); + } + } + + private IEnumerable ConvertSyntax() + { + for (int i = 0; i < MethodSymbol.Parameters.Length; i++) + { + IParameterSymbol arg = MethodSymbol.Parameters[i]; + yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + ParemeterSignatures[i].MarshalerType.AsTypeSyntax(), IdentifierName(ParemeterSignatures[i].ToJsMethod))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName((ParemeterSignatures[i].NeedsCast ? "___" : "")+ arg.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(ElementAccessExpression(IdentifierName("__args")) + .WithArgumentList(BracketedArgumentList(SingletonSeparatedList( + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i+1)))))))})))); + } + } + + private IEnumerable AfterSyntax() + { + for (int i = 0; i < MethodSymbol.Parameters.Length; i++) + { + if (ParemeterSignatures[i].AfterToJsMethod != null) + { + IParameterSymbol arg = MethodSymbol.Parameters[i]; + yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + ParemeterSignatures[i].MarshalerType.AsTypeSyntax(), IdentifierName(ParemeterSignatures[i].AfterToJsMethod))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName((ParemeterSignatures[i].NeedsCast ? "___" : "")+ arg.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(ElementAccessExpression(IdentifierName("__args")) + .WithArgumentList(BracketedArgumentList(SingletonSeparatedList( + Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i+1)))))))})))); + } + } + } + + private IEnumerable CallSyntax() + { + yield return ExpressionStatement(InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Constants.JavaScriptMarshalGlobal), IdentifierName("InvokeBoundJSFunction"))) + .WithArgumentList(ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(BindingName)), + Argument(IdentifierName("__args"))})))); + } + + private IEnumerable ReturnSyntax() + { + if (!IsVoidMethod) + { + ExpressionSyntax invocation = InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + ReturnSignature.MarshalerType.AsTypeSyntax(), IdentifierName(ReturnSignature.ToManagedMethod))) + .WithArgumentList(ArgumentList(SingletonSeparatedList( + Argument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("__args"), IdentifierName("Result")))))); + + if (ReturnSignature.NeedsCast) + { + invocation = CastExpression(MethodSymbol.ReturnType.AsTypeSyntax(), invocation); + } + + yield return ReturnStatement(invocation); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerMetadata.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerMetadata.cs new file mode 100644 index 00000000000000..59ebca802c9937 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerMetadata.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace JavaScript.MarshalerGenerator +{ + public class JSMarshalerMetadata + { + public ITypeSymbol MarshaledType; + public ITypeSymbol MarshalerType; + public string ToManagedMethod; + public string ToJsMethod; + public string AfterToJsMethod; + public bool IsAuto; + + public bool IsExactMatch(ITypeSymbol other) + { + Debug.Assert(MarshaledType != null); + return SymbolEqualityComparer.Default.Equals(MarshaledType, other); + } + + public bool IsAssignableFrom(Compilation compilation, ITypeSymbol argType) + { + Debug.Assert(compilation != null); + Debug.Assert(argType != null); + Debug.Assert(MarshaledType != null); + // TODO what about VB ? + return ((CSharpCompilation)compilation).ClassifyConversion(argType, MarshaledType).IsImplicit; + } + + public JSMarshalerSig ToSignature(bool needsCast) + { + var sig = new JSMarshalerSig + { + MarshaledType = MarshaledType, + MarshalerType = MarshalerType, + ToManagedMethod = ToManagedMethod, + ToJsMethod = ToJsMethod, + AfterToJsMethod = AfterToJsMethod, + IsAuto = IsAuto, + NeedsCast = needsCast, + }; + + return sig; + } + + public override string ToString() => $"MarshaledType:{MarshaledType} MarshalerType:{MarshalerType} ToManagedMethod:{ToManagedMethod} ToJsMethod:{ToJsMethod} IsAuto:{IsAuto}"; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerSig.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerSig.cs new file mode 100644 index 00000000000000..5f8c9c133f8b2b --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JSMarshalerSig.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace JavaScript.MarshalerGenerator +{ + public class JSMarshalerSig + { + public ITypeSymbol MarshaledType; + public ITypeSymbol MarshalerType; + public string ToManagedMethod; + public string ToJsMethod; + public string AfterToJsMethod; + public bool IsAuto; + public bool NeedsCast; + + public override string ToString() => $"MarshaledType:{MarshaledType} MarshalerType:{MarshalerType} ToManagedMethod:{ToManagedMethod} ToJsMethod:{ToJsMethod} IsAuto:{IsAuto}"; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JavaScript.MarshalerGenerator.csproj b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JavaScript.MarshalerGenerator.csproj new file mode 100644 index 00000000000000..23c1d2a7bcf1e7 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/JavaScript.MarshalerGenerator.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + enable + true + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/MarshalerSelector.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/MarshalerSelector.cs new file mode 100644 index 00000000000000..ec8febd986c4c4 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/MarshalerSelector.cs @@ -0,0 +1,279 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace JavaScript.MarshalerGenerator +{ + internal class MarshalerSelector + { + private JSMarshalerMetadata Void; + private JSMarshalerMetadata Object; + private JSMarshalerMetadata JSObject; + private JSMarshalerMetadata Exception; + private JSMarshalerMetadata Task; + + private Compilation Compilation; + + + internal JSMarshalerSig GetArgumentSignature(StringBuilder prolog, JSMarshalerMetadata[] customMarshalers, ITypeSymbol argType) + { + foreach (var marshaler in marshalers) + { + if (marshaler.IsExactMatch(argType)) + { + return marshaler.ToSignature(false); + }; + } + + if (customMarshalers != null) foreach (var custom in customMarshalers) + { + if (custom.IsAssignableFrom(Compilation, argType)) + { + return custom.ToSignature(!custom.IsExactMatch(argType)); + } + } + + if (JSObject.IsAssignableFrom(Compilation, argType)) return JSObject.ToSignature(!JSObject.IsExactMatch(argType)); + if (Exception.IsAssignableFrom(Compilation, argType)) return Exception.ToSignature(!Exception.IsExactMatch(argType)); + if (Task.IsAssignableFrom(Compilation, argType)) return Task.ToSignature(!Task.IsExactMatch(argType)); + + // TODO check if it has MarshalAs or StructLayout + // TODO test enums, ... + // TODO test Nullable + if (argType.IsValueType) + { + throw new NotSupportedException("TODO, struct is not supported: "+ argType.AsTypeSyntax()); + } + + // classes via System.Object reference + return Object.ToSignature(!Object.IsExactMatch(argType)); + } + + private List marshalers = new List(); + private JSMarshalerMetadata AddMarshaler(JSMarshalerMetadata meta) + { + marshalers.Add(meta); + return meta; + } + + public MarshalerSelector(Compilation compilation) + { + Compilation = compilation; + + AddPrimitive(compilation); + AddNullable(compilation); + + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_String), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedString", + ToJsMethod = "MarshalStringToJs", + }); + + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_DateTime), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedDateTime", + ToJsMethod = "MarshalDateTimeToJs", + }); + + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.DateTimeOffset"), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedDateTimeOffset", + ToJsMethod = "MarshalDateTimeOffsetToJs", + }); + + Task = AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedTask", + ToJsMethod = "MarshalTaskToJs", + AfterToJsMethod = "AfterMarshalTaskToJs", + }); + + JSObject = AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".IJSObject"), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedIJSObject", + ToJsMethod = "MarshalIJSObjectToJs", + }); + + Exception = AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Exception"), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedException", + ToJsMethod = "MarshalExceptionToJs", + }); + + Object = AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Object), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedObject", + ToJsMethod = "MarshalObjectToJs", + }); + + Void = AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Void), + MarshalerType = null, + ToManagedMethod = null, + ToJsMethod = null, + }); + } + + private void AddNullable(Compilation compilation) { + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Boolean)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Boolean)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Byte)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Byte)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Int16)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Int16)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Int32)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Int32)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Int64)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Int64)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Single)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Single)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_Double)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_Double)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_IntPtr)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_IntPtr)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetSpecialType(SpecialType.System_DateTime)), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetSpecialType(SpecialType.System_DateTime)), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetTypeByMetadataName("System.Nullable`1").Construct(compilation.GetTypeByMetadataName("System.DateTimeOffset")), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptPublic + ".NullableMarshaler`1").Construct(compilation.GetTypeByMetadataName("System.DateTimeOffset")), + ToManagedMethod = "MarshalToManaged", + ToJsMethod = "MarshalToJs", + IsAuto = true + }); + } + + private void AddPrimitive(Compilation compilation) + { + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Boolean), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedBoolean", + ToJsMethod = "MarshalBooleanToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Byte), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedByte", + ToJsMethod = "MarshalByteToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Int16), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedInt16", + ToJsMethod = "MarshalInt16ToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Int32), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedInt32", + ToJsMethod = "MarshalInt32ToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Int64), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedInt64", + ToJsMethod = "MarshalInt64ToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Single), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedSingle", + ToJsMethod = "MarshalSingleToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_Double), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedDouble", + ToJsMethod = "MarshalDoubleToJs", + }); + AddMarshaler(new JSMarshalerMetadata + { + MarshaledType = compilation.GetSpecialType(SpecialType.System_IntPtr), + MarshalerType = compilation.GetTypeByMetadataName(Constants.JavaScriptMarshal), + ToManagedMethod = "MarshalToManagedIntPtr", + ToJsMethod = "MarshalIntPtrToJs", + }); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/TypeSymbolExtensions.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/TypeSymbolExtensions.cs new file mode 100644 index 00000000000000..03a26facb2ad3e --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/gen/MarshalerGenerator/TypeSymbolExtensions.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace JavaScript.MarshalerGenerator +{ + public static class TypeSymbolExtensions + { + public static TypeSyntax AsTypeSyntax(this ITypeSymbol type) + { + string text = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return ParseTypeName(text); + } + + public static NamespaceDeclarationSyntax AsNamespace(this INamespaceSymbol ns) + { + string text = ns.ToDisplayString(); + return NamespaceDeclaration(ParseName(text)); + } + } +} 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 af10e0fd5fd6af..4e71d459c260d5 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 @@ -4,13 +4,42 @@ enable $(NetCoreAppCurrent)-Browser $(NoWarn);CA1419 + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -19,6 +48,10 @@ + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.CustomMarshalers.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.CustomMarshalers.cs new file mode 100644 index 00000000000000..98ffe3effa2720 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.CustomMarshalers.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal partial class JavaScriptMarshalImpl + { + internal static void RegisterCustomMarshalers(JavaScriptMarshalerBase[] customMarshallers) + { + if (customMarshallers == null) + { + return; + } + for (int i = 0; i < customMarshallers.Length; i++) + { + JavaScriptMarshalerBase marshaler = customMarshallers[i]; + if (marshaler.MarshallerJSHandle == IntPtr.Zero) + { + string? code = marshaler.GetJavaScriptCode(); + if (code == null) throw new ArgumentException("JavaScriptCode"); + + var type = marshaler.MarshaledType.TypeHandle.Value; + var exceptionMessage = _RegisterCustomMarshaller(code, type, out var jsHandle, out var isException); + if (isException != 0) + { + throw new JSException(exceptionMessage); + } + marshaler.MarshallerJSHandle = jsHandle; + } + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.Signature.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.Signature.cs new file mode 100644 index 00000000000000..b8c4c4dcb7f896 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.Signature.cs @@ -0,0 +1,223 @@ +// 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.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal static unsafe partial class JavaScriptMarshalImpl + { + internal static JSMarshalerSig exceptionSignature = GetExceptionSignature(); + internal static JSMarshalerSig voidSignature = GetVoidSignature(); + + internal static IntPtr boolType = typeof(bool).TypeHandle.Value; + internal static IntPtr byteType = typeof(byte).TypeHandle.Value; + internal static IntPtr int16Type = typeof(short).TypeHandle.Value; + internal static IntPtr int32Type = typeof(int).TypeHandle.Value; + internal static IntPtr int64Type = typeof(long).TypeHandle.Value; + internal static IntPtr floatType = typeof(float).TypeHandle.Value; + internal static IntPtr doubleType = typeof(double).TypeHandle.Value; + internal static IntPtr intptrType = typeof(IntPtr).TypeHandle.Value; + internal static IntPtr dateTimeType = typeof(DateTime).TypeHandle.Value; + internal static IntPtr dateTimeOffsetType = typeof(DateTimeOffset).TypeHandle.Value; + + internal static IntPtr stringType = typeof(string).TypeHandle.Value; + internal static IntPtr ijsObjectType = typeof(IJSObject).TypeHandle.Value; + internal static IntPtr objectType = typeof(object).TypeHandle.Value; + internal static IntPtr exceptionType = typeof(Exception).TypeHandle.Value; + internal static IntPtr taskType = typeof(Task).TypeHandle.Value; + internal static IntPtr tcsType = typeof(TaskCompletionSource).TypeHandle.Value; + internal static IntPtr jsExceptionType = typeof(JSException).TypeHandle.Value; + + private static Dictionary methodSignatures = new Dictionary(new TypeArrayEqualityComparer()); + + internal static JSMarshalerSig GetExceptionSignature() + { + return new JSMarshalerSig + { + TypeHandle = typeof(Exception).TypeHandle.Value, + BufferLength = -1, + BufferOffset = -1, + UseRoot = 0, + MarshallerJSHandle = IntPtr.Zero, + }; + } + + internal static JSMarshalerSig GetVoidSignature() + { + return new JSMarshalerSig + { + TypeHandle = IntPtr.Zero, + BufferLength = -1, + BufferOffset = -1, + UseRoot = 0, + MarshallerJSHandle = IntPtr.Zero, + }; + } + + internal static JSMarshalerSig GetArgumentSignature(JavaScriptMarshalerBase[] customMarshalers, Type argType, ref int extraBufferLength) + { + if (argType.IsPrimitive) + { + return new JSMarshalerSig + { + TypeHandle = argType.TypeHandle.Value, + BufferLength = -1, + BufferOffset = -1, + UseRoot = 0, + MarshallerJSHandle = IntPtr.Zero, + }; + } + if (marshalers.TryGetValue(argType, out var m1)) + { + int argBufferLength = m1.GetFixedBufferLength(); + var sig = new JSMarshalerSig + { + TypeHandle = m1.MarshaledType.TypeHandle.Value, + BufferLength = argBufferLength != 0 + ? argBufferLength + : -1, + BufferOffset = argBufferLength == 0 ? -1 : extraBufferLength, + UseRoot = m1.GetUseRoot() ? 1 : 0, + MarshallerJSHandle = m1.MarshallerJSHandle, + }; + extraBufferLength += argBufferLength; + return sig; + } + if (customMarshalers != null) foreach (var m3 in customMarshalers) + { + if (m3.MarshaledType.IsAssignableFrom(argType)) + { + int argBufferLength = m3.GetFixedBufferLength(); + var sig = new JSMarshalerSig + { + TypeHandle = m3.MarshaledType.TypeHandle.Value, + BufferLength = argBufferLength != 0 + ? argBufferLength + : -1, + BufferOffset = argBufferLength == 0 ? -1 : extraBufferLength, + UseRoot = m3.GetUseRoot() ? 1 : 0, + MarshallerJSHandle = m3.MarshallerJSHandle, + }; + extraBufferLength += argBufferLength; + return sig; + } + } + foreach (var m2 in marshalersSequence) + { + if (m2.MarshaledType.IsAssignableFrom(argType)) + { + int argBufferLength = m2.GetFixedBufferLength(); + var sig = new JSMarshalerSig + { + TypeHandle = m2.MarshaledType.TypeHandle.Value, + BufferLength = argBufferLength != 0 + ? argBufferLength + : -1, + BufferOffset = argBufferLength == 0 ? -1 : extraBufferLength, + UseRoot = m2.GetUseRoot() ? 1 : 0, + MarshallerJSHandle = m2.MarshallerJSHandle, + }; + extraBufferLength += argBufferLength; + return sig; + } + } + // fallback System.Object + return new JSMarshalerSig + { + TypeHandle = objectType, + BufferLength = -1, + BufferOffset = -1, + MarshallerJSHandle = IntPtr.Zero, + UseRoot = 1, + }; + } + // res type is first + internal static JavaScriptMarshalerSignature GetMethodSignature(JavaScriptMarshalerBase[] customMarshalers, params Type[] types) + { + + lock (methodSignatures) + { + // TODO the cache doesn't differentiate customMarshalers, just the types they marshal + if (methodSignatures.TryGetValue(types, out var signature)) + { + // copy as it could have different JSHandle + return new JavaScriptMarshalerSignature + { + Header = signature.Header, + Sigs = signature.Sigs, + CustomMarshalers = signature.CustomMarshalers, + Types = signature.Types, + }; + } + + int argsCount = types.Length - 1; + var size = sizeof(JSMarshalerSignatureHeader) + ((2 + argsCount) * sizeof(JSMarshalerSig)); + // this is allocated outside of GC, so that it doesn't move in memory. We send it to JS side. + var buffer = Marshal.AllocHGlobal(size); + var header = (JSMarshalerSignatureHeader*)buffer; + var args = (JSMarshalerSig*)(buffer + sizeof(JSMarshalerSignatureHeader)); + signature = new JavaScriptMarshalerSignature + { + Header = (JSMarshalerSignatureHeader*)buffer, + Sigs = (JSMarshalerSig*)(buffer + sizeof(JSMarshalerSignatureHeader)), + CustomMarshalers = customMarshalers, + Types = types, + }; + + var extraBufferLength = 0; + signature.ArgumentCount = argsCount; + signature.Exception = exceptionSignature; + var resType = types[0]; + signature.Result = resType == typeof(void) + ? voidSignature + : GetArgumentSignature(customMarshalers, resType, ref extraBufferLength); + for (int i = 0; i < argsCount; i++) + { + args[i] = GetArgumentSignature(customMarshalers, types[i+1], ref extraBufferLength); + } + signature.ExtraBufferLength = extraBufferLength; + methodSignatures.Add(types, signature); + return signature; + } + } + + internal class TypeArrayEqualityComparer : IEqualityComparer + { + public bool Equals(Type[]? first, Type[]? second) + { + if (first == second) + { + return true; + } + if (first == null || second == null) + { + return false; + } + if (first.Length != second.Length) + { + return false; + } + for (int i = 0; i < first.Length; i++) + { + if (first[i] != second[i]) + { + return false; + } + } + return true; + } + + public int GetHashCode(Type[] obj) + { + var hc = default(HashCode); + foreach (var type in obj) + { + hc.Add(type); + } + return hc.ToHashCode(); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.cs new file mode 100644 index 00000000000000..fe9dd90d4591aa --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.cs @@ -0,0 +1,67 @@ +// 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.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal static unsafe partial class JavaScriptMarshalImpl + { + internal static Function log = new Function("data", @"console.log(data)"); + private static Dictionary marshalers = new Dictionary(); + private static List marshalersSequence = new List(); + + static JavaScriptMarshalImpl() + { + ExceptionMarshaler exception = new(); + JSObjectMarshaler jsObject = new(); + SystemObjectMarshaler sysObject = new SystemObjectMarshaler(); + StringMarshaler str = new(); + TaskMarshaler task = new(); + DateTimeMarshaler datetime = new(); + DateTimeOffsetMarshaler datetimeOffset = new(); + + lock (marshalers) + { + marshalers.Add(typeof(string), str); + marshalers.Add(typeof(DateTime), datetime); + marshalers.Add(typeof(DateTimeOffset), datetimeOffset); + + marshalersSequence.Add(jsObject); + marshalers.Add(typeof(JSObject), jsObject); + + marshalersSequence.Add(exception); + marshalers.Add(typeof(JSException), exception); + + marshalersSequence.Add(task); + marshalers.Add(typeof(Task), task); + + marshalersSequence.Add(sysObject); + marshalers.Add(typeof(object), sysObject); + } + } + + private static void RegisterMarshaler(JavaScriptMarshalerBase marshaler) + { + lock (marshalers) + { + if (!marshaler.MarshaledType.IsSealed) + { + marshalersSequence.Insert(0, marshaler); + } + marshalers.Add(marshaler.MarshaledType, marshaler); + } + } + + internal static void ThrowException(JavaScriptMarshalerArg arg) + { + var ex = JavaScriptMarshal.MarshalToManagedException(arg); + if (ex != null) + { + throw ex; + } + throw new JSException("unknown exception"); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.icalls.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.icalls.cs new file mode 100644 index 00000000000000..aa933f1e192dee --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/JavaScriptMarshalImpl.icalls.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal static unsafe partial class JavaScriptMarshalImpl + { + [MethodImpl(MethodImplOptions.InternalCall)] + internal static unsafe extern string _BindJSFunction(string function_name, void* signature, out IntPtr bound_function_js_handle, out int is_exception); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void _InvokeBoundJSFunction(IntPtr bound_function_js_handle, void* data); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static unsafe extern string _BindCSFunction(string fully_qualified_name, int signature_hash, string export_as_name, void* signature, out int is_exception); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static unsafe extern string _RegisterCustomMarshaller(string factory_code, IntPtr type_handle, out IntPtr js_handle_out, out int is_exception); + + [JSImport("INTERNAL.mono_wasm_resolve_task")] + internal static partial void _ResolveTask(IntPtr gc_task_handle, object data); + + [JSImport("INTERNAL.mono_wasm_reject_task")] + internal static partial void _RejectTask(IntPtr gc_task_handle, Exception reason); + + + internal static readonly Dictionary> _jsHandleToTaskCompletionSource = new Dictionary>(); + + [JSExport("INTERNAL.mono_wasm_resolve_tcs")] + private static void _ResolveTaskCompletionSource(IntPtr js_tcs_handle, object data) + { + TaskCompletionSource tcs = null; + lock (_jsHandleToTaskCompletionSource) + { + _jsHandleToTaskCompletionSource.Remove(js_tcs_handle, out tcs); + } + if (tcs!= null) + { + tcs.SetResult(data); + } + } + + [JSExport("INTERNAL.mono_wasm_reject_tcs")] + private static void _RejectTaskCompletionSource(IntPtr js_tcs_handle, Exception reason) + { + TaskCompletionSource tcs = null; + lock (_jsHandleToTaskCompletionSource) + { + _jsHandleToTaskCompletionSource.Remove(js_tcs_handle, out tcs); + } + if (tcs != null) + { + tcs.SetException(reason); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/DateTimeMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/DateTimeMarshaler.cs new file mode 100644 index 00000000000000..d6a6c81ef61cf8 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/DateTimeMarshaler.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class DateTimeMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedDateTime; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalDateTimeToJs; + } + + internal sealed class DateTimeOffsetMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedDateTimeOffset; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalDateTimeOffsetToJs; + } +} + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe DateTime MarshalToManagedDateTime(JavaScriptMarshalerArg arg) + { + return DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value).UtcDateTime; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalDateTimeToJs(ref DateTime value, JavaScriptMarshalerArg arg) + { + // ToUnixTimeMilliseconds is always UTC + arg.Int64Value = new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe DateTimeOffset MarshalToManagedDateTimeOffset(JavaScriptMarshalerArg arg) + { + return DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalDateTimeOffsetToJs(ref DateTimeOffset value, JavaScriptMarshalerArg arg) + { + // ToUnixTimeMilliseconds is always UTC + arg.Int64Value = value.ToUnixTimeMilliseconds(); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/ExceptionMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/ExceptionMarshaler.cs new file mode 100644 index 00000000000000..9d092125b1cbc4 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/ExceptionMarshaler.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class ExceptionMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override bool UseRoot => true; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedException; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalExceptionToJs; + } +} +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Exception MarshalToManagedException(JavaScriptMarshalerArg arg) + { + // Always creating stack trace is more expensive than passing js_handle and GC of JSObject. + // Same in reverse direcion with gc_handle and ManagedObject. + // The Exception/JSException instances roundtip and keep identity. + + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + if (arg.TypeHandle == JavaScriptMarshalImpl.exceptionType) + { + // this is managed exception round-trip + return (Exception)((GCHandle)arg.GCHandle).Target; + } + + JSObject jsException = null; + if (arg.JSHandle != IntPtr.Zero) + { + // this is JSException round-trip + jsException = Runtime.CreateCSOwnedProxy(arg.JSHandle, Runtime.MappedType.JSObject, 0); + + if (jsException == null) + { + jsException = new JSObject(arg.JSHandle); ; + } + } + + var message = Unsafe.AsRef(arg.RootRef.ToPointer()); + var ex = new JSException(message, jsException); + return ex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalExceptionToJs(ref Exception value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + } + else + { + var jse = value as JSException; + if (jse != null && jse.jsException != null) + { + // this is JSException roundtrip + arg.TypeHandle = JavaScriptMarshalImpl.jsExceptionType; + arg.JSHandle = (IntPtr)jse.jsException.JSHandle; + } + else + { + arg.TypeHandle = JavaScriptMarshalImpl.exceptionType; + arg.GCHandle = (IntPtr)Runtime.GetJSOwnedObjectGCHandle(value); + } + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/JSObjectMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/JSObjectMarshaler.cs new file mode 100644 index 00000000000000..7e4d147d2ecafa --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/JSObjectMarshaler.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class JSObjectMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedIJSObject; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalIJSObjectToJs; + } +} + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe IJSObject MarshalToManagedIJSObject(JavaScriptMarshalerArg arg) + { + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + + return Runtime.CreateCSOwnedProxy(arg.JSHandle, Runtime.MappedType.JSObject, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalIJSObjectToJs(ref IJSObject value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + } + else + { + arg.TypeHandle = JavaScriptMarshalImpl.ijsObjectType; + arg.JSHandle = (IntPtr)((JSObject)value).JSHandle; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/StringMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/StringMarshaler.cs new file mode 100644 index 00000000000000..3b65bf55591cbb --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/StringMarshaler.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class StringMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override bool UseRoot => true; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedString; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalStringToJs; + } +} + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe string MarshalToManagedString(JavaScriptMarshalerArg arg) + { + // We marshal the string by passing MonoString* to JS side and convert it there. + // MonoString* is rooted on C# stack because string is ref type + // We don't need GCHandle here because we then covert it by value (or reuse interned JS string). + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + return Unsafe.AsRef(arg.RootRef.ToPointer()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalStringToJs(ref string value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + } + else + { + SetRootRef(ref value, arg); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/SystemObjectMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/SystemObjectMarshaler.cs new file mode 100644 index 00000000000000..efce2b2c687a72 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/SystemObjectMarshaler.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class SystemObjectMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override bool UseRoot => true;// the actual marshaled type could be Exception or MonoString, which needs root + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedObject; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalObjectToJs; + } +} + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + // TODO does this need to handle custom marshalers ? + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe object MarshalToManagedObject(JavaScriptMarshalerArg arg) + { + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + if (arg.TypeHandle == JavaScriptMarshalImpl.objectType) + { + return ((GCHandle)arg.GCHandle).Target; + } + if (arg.TypeHandle == JavaScriptMarshalImpl.int64Type) + { + return MarshalToManagedInt64(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.int32Type) + { + return MarshalToManagedInt32(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.int16Type) + { + return MarshalToManagedInt16(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.byteType) + { + return MarshalToManagedByte(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.boolType) + { + return MarshalToManagedBoolean(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.doubleType) + { + return MarshalToManagedDouble(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.floatType) + { + return MarshalToManagedSingle(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.intptrType) + { + return MarshalToManagedIntPtr(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.ijsObjectType) + { + return MarshalToManagedIJSObject(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.stringType) + { + return MarshalToManagedString(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.exceptionType) + { + return MarshalToManagedException(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.exceptionType) + { + return MarshalToManagedException(arg); + } + if (arg.TypeHandle == JavaScriptMarshalImpl.dateTimeType) + { + return DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value).UtcDateTime; + } + if (arg.TypeHandle == JavaScriptMarshalImpl.dateTimeOffsetType) + { + return DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value); + } + + throw new NotImplementedException("MarshalToManagedObject: " + arg.TypeHandle); + } + + // TODO does this need to handle custom marshalers ? + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void MarshalObjectToJs(ref object value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + return; + } + + Type type = value.GetType(); + if (type.IsPrimitive) + { + arg.TypeHandle = type.TypeHandle.Value; + if (typeof(long) == type) + { + arg.Int64Value = (long)value; + } + else if (typeof(int) == type) + { + arg.Int32Value = (int)value; + } + else if (typeof(short) == type) + { + arg.Int16Value = (short)value; + } + else if (typeof(byte) == type) + { + arg.ByteValue = (byte)value; + } + else if (typeof(bool) == type) + { + arg.BooleanValue = (bool)value; + } + else if (typeof(double) == type) + { + arg.DoubleValue = (double)value; + } + else if (typeof(float) == type) + { + arg.SingleValue = (float)value; + } + else if (typeof(IntPtr) == type) + { + arg.IntPtrValue = (IntPtr)value; + } + else + { + throw new NotImplementedException(type.FullName); + } + } + else if (typeof(DateTimeOffset) == type) + { + arg.TypeHandle = JavaScriptMarshalImpl.dateTimeOffsetType; + arg.Int64Value = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); + } + else if (typeof(DateTime) == type) + { + arg.TypeHandle = JavaScriptMarshalImpl.dateTimeType; + arg.Int64Value = new DateTimeOffset((DateTime)value).ToUnixTimeMilliseconds(); + } + else if (typeof(JSObject).IsAssignableFrom(type)) + { + arg.TypeHandle = JavaScriptMarshalImpl.ijsObjectType; + arg.JSHandle = (IntPtr)((JSObject)value).JSHandle; + } + else if (typeof(Exception).IsAssignableFrom(type)) + { + var ex = (Exception)value; + MarshalExceptionToJs(ref ex, arg); + } + else if (!type.IsValueType) + { + arg.TypeHandle = JavaScriptMarshalImpl.objectType; + arg.GCHandle = (IntPtr)Runtime.GetJSOwnedObjectGCHandle(value); + } + else if (Nullable.GetUnderlyingType(type) != null) + { + // TODO auto custom marshaler ? + throw new NotImplementedException(type.FullName); + } + else + { + //TODO P/Invoke like marshaling of struct + throw new NotImplementedException(type.FullName); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/TaskMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/TaskMarshaler.cs new file mode 100644 index 00000000000000..09a01ee94c27c9 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Marshalers/TaskMarshaler.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + internal sealed class TaskMarshaler : JavaScriptMarshalerBase + { + protected override string JavaScriptCode => null; + protected override MarshalToManagedDelegate ToManaged => JavaScriptMarshal.MarshalToManagedTask; + protected override MarshalToJavaScriptDelegate ToJavaScript => JavaScriptMarshal.MarshalTaskToJs; + protected override MarshalToJavaScriptDelegate AfterToJavaScript => JavaScriptMarshal.MarshalTaskToJs; + } +} + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Task MarshalToManagedTask(JavaScriptMarshalerArg arg) + { + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + /*if (arg.TypeHandle == JavaScriptMarshalImpl.taskType) + { + // this is managed Task round-trip + var t=((GCHandle)arg.GCHandle); + return (Task)t.Target; + } + */ + Debug.Assert(arg.TypeHandle == JavaScriptMarshalImpl.ijsObjectType); + + IntPtr jSHandle = arg.JSHandle; + TaskCompletionSource tcs = new TaskCompletionSource(jSHandle); + lock (JavaScriptMarshalImpl._jsHandleToTaskCompletionSource) + { + JavaScriptMarshalImpl._jsHandleToTaskCompletionSource.Add(jSHandle, tcs); + } + return tcs.Task; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalTaskToJs(ref Task value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + return; + } + /*if (value.AsyncState is IntPtr jsHandle) + { + // This is Promise round-trip + arg.TypeHandle = JavaScriptMarshalImpl.ijsObjectType; + arg.JSHandle = jsHandle; + } + else + {*/ + var gcHandle = (IntPtr)GCHandle.Alloc(value, GCHandleType.Normal); + arg.TypeHandle = JavaScriptMarshalImpl.taskType; + arg.GCHandle = gcHandle; + //} + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AfterMarshalTaskToJs(ref Task value, JavaScriptMarshalerArg arg) + { + var task = value; + if (arg.TypeHandle != JavaScriptMarshalImpl.taskType) + { + return; + } + // we know that task instance is still alive + // we also know that gcHandle is valid, until Complete() was called + // because we need to make sure that GCHandle is not dealocated before Complete() + // un-completed Tasks leak resources + var gcHandle = arg.GCHandle; + + if (task.IsCompleted) + Complete(); + else + task.GetAwaiter().OnCompleted(Complete); + + void Complete() + { + try + { + if (task.Exception == null) + { + object result; + Type task_type = task.GetType(); + if (task_type == typeof(Task) || task == Task.CompletedTask) + { + result = null; + } + else + { + // TODO, is this reflection slow ? + result = Runtime.GetTaskResultMethodInfo(task_type)?.Invoke(task, null); + } + + JavaScriptMarshalImpl._ResolveTask(gcHandle, result); + } + else + { + if (task.Exception is AggregateException ae && ae.InnerExceptions.Count == 1) + { + JavaScriptMarshalImpl._RejectTask(gcHandle, ae.InnerExceptions[0]); + } + else + { + JavaScriptMarshalImpl._RejectTask(gcHandle, task.Exception); + } + } + GCHandle gch = (GCHandle)gcHandle; + gch.Free(); + } + catch (Exception ex) + { + throw new InvalidOperationException("Should not happen", ex); + } + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArg.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArg.cs new file mode 100644 index 00000000000000..055423740b4278 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArg.cs @@ -0,0 +1,78 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial struct JavaScriptMarshalerArg + { + internal unsafe JSMarshalerArg* Value; + + public override unsafe string ToString() + { + return Value == null ? "null" : Value[0].ToString(); + } + } +} + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + /// + /// Discriminated union + /// + [StructLayout(LayoutKind.Explicit, Pack = 8)] + public struct JSMarshalerArg + { + #region Primitive + [FieldOffset(0)] + public bool BooleanValue; + [FieldOffset(0)] + public byte ByteValue; + [FieldOffset(0)] + public short Int16Value; + [FieldOffset(0)] + public int Int32Value; + [FieldOffset(0)] + public long Int64Value;// must be alligned to 8 because of HEAPI64 alignment + [FieldOffset(0)] + public float SingleValue; + [FieldOffset(0)] + public double DoubleValue;// must be alligned to 8 because of Module.HEAPF64 view alignment + [FieldOffset(0)] + public IntPtr IntPtrValue; + #endregion + + #region Custom + [FieldOffset(0)] + public IntPtr ExtraBufferPtr; + #endregion + + #region JSObject, JSException + [FieldOffset(4)] + public IntPtr JSHandle; + #endregion + + #region System.Object, System.Exception + [FieldOffset(4)] + public IntPtr GCHandle; + #endregion + + #region JSException, System.Exception + [FieldOffset(8)] + public IntPtr RootRef; + #endregion + + /// + /// Discriminator + /// + [FieldOffset(12)] + public IntPtr TypeHandle; + + public override unsafe string ToString() + { + return $"JSArg - TypeHandle:{TypeHandle} RootRef/Int32Value:{Int32Value} DoubleValue:{DoubleValue} JSHandle/GCHandle{JSHandle}"; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArguments.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArguments.cs new file mode 100644 index 00000000000000..92715a35588d40 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerArguments.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial struct JavaScriptMarshalerArguments + { + internal unsafe void* Buffer; + } +} + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public struct JSMarshalerArgumentsHeader + { + public JSMarshalerArg Exception; + public JSMarshalerArg Result; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSig.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSig.cs new file mode 100644 index 00000000000000..b19b7d97f05209 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSig.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices.JavaScript.Private +{ + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct JSMarshalerSignatureHeader + { + public int ArgumentCount; + public int ExtraBufferLength; + public JSMarshalerSig Exception; + public JSMarshalerSig Result; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)] + public struct JSMarshalerSig + { + public IntPtr TypeHandle; + public int BufferOffset; + public int BufferLength; + public int UseRoot; + public IntPtr MarshallerJSHandle; + + public override string ToString() + { + return $"Type: {TypeHandle} BufferOffset: {BufferOffset}, BufferLength:{BufferLength}, UseRoot:{UseRoot}"; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSignature.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSignature.cs new file mode 100644 index 00000000000000..8b8c6748ab59b0 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Private/Structures/JavaScriptMarshalerSignature.cs @@ -0,0 +1,93 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; +#nullable disable + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshalerSignature + { + internal unsafe JSMarshalerSignatureHeader* Header; + internal unsafe JSMarshalerSig* Sigs; + internal IntPtr JSHandle; + internal JavaScriptMarshalerBase[] CustomMarshalers; + internal Type[] Types; + + internal unsafe int ArgumentCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Header[0].ArgumentCount; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Header[0].ArgumentCount = value; + } + } + + internal unsafe int ExtraBufferLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Header[0].ExtraBufferLength; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Header[0].ExtraBufferLength = value; + } + } + + internal unsafe int ArgumentsBufferLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return sizeof(JSMarshalerArgumentsHeader) + ArgumentCount * sizeof(JSMarshalerArg); + } + } + + internal unsafe JSMarshalerSig Result + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Header[0].Result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Header[0].Result = value; + } + } + + internal unsafe JSMarshalerSig Exception + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Header[0].Exception; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Header[0].Exception = value; + } + } + + // one based position of args, not exception, not result + internal unsafe JSMarshalerSig this[int position] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Sigs[position - 1]; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/IJSObject.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/IJSObject.cs new file mode 100644 index 00000000000000..1df002a1de2182 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/IJSObject.cs @@ -0,0 +1,12 @@ +// 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.Versioning; + +namespace System.Runtime.InteropServices.JavaScript +{ + [SupportedOSPlatform("browser")] + public interface IJSObject + { + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSException.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSException.cs new file mode 100644 index 00000000000000..5aa5aba9600903 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSException.cs @@ -0,0 +1,79 @@ +// 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.Versioning; + +namespace System.Runtime.InteropServices.JavaScript +{ + /// + /// Represents an Exception initiated from the JavaScript interop code. + /// + [SupportedOSPlatform("browser")] + public class JSException : Exception + { + internal JSObject? jsException; + public JSException(string msg) : base(msg) + { + jsException = null; + } + internal JSException(string msg, JSObject? jsException) : base(msg) + { + this.jsException = jsException; + } + + public override string StackTrace + { + get + { + var bs = base.StackTrace; +#nullable disable + if (jsException == null) + { + return bs; + } + var jsStackTrace = (string)jsException.GetObjectProperty("stack"); + if (jsStackTrace == null) + { + if (bs == null) + { + return null; + } + } + else if (jsStackTrace.StartsWith(Message + "\n")) + { + // Some JS runtimes insert the error message at the top of the stack, some don't, + // so normalize it by using the stack as the result if it already contains the error + jsStackTrace = jsStackTrace.Substring(Message.Length + 1); + } + + if (bs == null) + { + return jsStackTrace; + } + return base.StackTrace + "\r\n" + jsStackTrace; +#nullable restore + } + + } + + public override bool Equals(object? obj) + { + var other = obj as JSException; + if (other == null) return false; + return other.jsException == jsException; + } + + public override int GetHashCode() + { + return jsException == null + ? base.GetHashCode() + : base.GetHashCode() * jsException.JSHandle; + } + + public override string ToString() + { + // skip the expensive stack trace + return Message; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSExportAttribute.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSExportAttribute.cs new file mode 100644 index 00000000000000..1e28da5fbd88d8 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSExportAttribute.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices.JavaScript +{ + /// + /// It will make the exported function not trimmable by AOT + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public sealed class JSExportAttribute : Attribute + { + public string? FunctionName { get; } + public Type[]? CustomMarshallers { get; } + + public JSExportAttribute() + { + FunctionName = null; + CustomMarshallers = null; + } + + public JSExportAttribute(string functionName) + { + FunctionName = functionName; + CustomMarshallers = null; + } + + public JSExportAttribute(string functionName, params Type[] customMarshallers) + { + FunctionName = functionName; + CustomMarshallers = customMarshallers; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSImportAttribute.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSImportAttribute.cs new file mode 100644 index 00000000000000..74362d52fd82c8 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JSImportAttribute.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.InteropServices.JavaScript +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public sealed class JSImportAttribute : Attribute + { + public string FunctionName { get; } + public Type[]? CustomMarshallers { get; } + + public JSImportAttribute(string functionName) + { + FunctionName = functionName; + CustomMarshallers = null; + } + + public JSImportAttribute(string functionName, params Type[] customMarshallers) + { + FunctionName = functionName; + CustomMarshallers = customMarshallers; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.Primitive.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.Primitive.cs new file mode 100644 index 00000000000000..84d9e7552aa184 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.Primitive.cs @@ -0,0 +1,108 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool MarshalToManagedBoolean(JavaScriptMarshalerArg arg) + { + return arg.BooleanValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalBooleanToJs(ref bool value, JavaScriptMarshalerArg arg) + { + arg.BooleanValue = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe byte MarshalToManagedByte(JavaScriptMarshalerArg arg) + { + return arg.ByteValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalByteToJs(ref byte value, JavaScriptMarshalerArg arg) + { + arg.ByteValue = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe short MarshalToManagedInt16(JavaScriptMarshalerArg arg) + { + return arg.Int16Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalInt16ToJs(ref short value, JavaScriptMarshalerArg arg) + { + arg.Int16Value = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int MarshalToManagedInt32(JavaScriptMarshalerArg arg) + { + return arg.Int32Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalInt32ToJs(ref int value, JavaScriptMarshalerArg arg) + { + arg.Int32Value = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe IntPtr MarshalToManagedIntPtr(JavaScriptMarshalerArg arg) + { + return arg.IntPtrValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalIntPtrToJs(ref IntPtr value, JavaScriptMarshalerArg arg) + { + arg.IntPtrValue = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe long MarshalToManagedInt64(JavaScriptMarshalerArg arg) + { + return arg.Int64Value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalInt64ToJs(ref long value, JavaScriptMarshalerArg arg) + { + arg.Int64Value = value; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe float MarshalToManagedSingle(JavaScriptMarshalerArg arg) + { + return arg.SingleValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalSingleToJs(ref float value, JavaScriptMarshalerArg arg) + { + arg.SingleValue = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe double MarshalToManagedDouble(JavaScriptMarshalerArg arg) + { + return arg.DoubleValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalDoubleToJs(ref double value, JavaScriptMarshalerArg arg) + { + arg.DoubleValue = value; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.cs new file mode 100644 index 00000000000000..a29629e33421fb --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshal.cs @@ -0,0 +1,136 @@ +// 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; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript +{ + [CLSCompliant(false)] + public unsafe partial class JavaScriptMarshal + { + public static JavaScriptMarshalerSignature BindCSFunction(string fullyQualifiedName, int signatureHash, string exportAsName, JavaScriptMarshalerBase[] customMarshalers, params Type[] types) + { + JavaScriptMarshalImpl.RegisterCustomMarshalers(customMarshalers); + var signature = JavaScriptMarshalImpl.GetMethodSignature(customMarshalers, types); + + var exceptionMessage = JavaScriptMarshalImpl._BindCSFunction(fullyQualifiedName, signatureHash, exportAsName, signature.Header, out int isException); + if (isException != 0) + { + throw new JSException(exceptionMessage); + } + return signature; + } + + public static JavaScriptMarshalerSignature BindJSFunction(string functionName, JavaScriptMarshalerBase[] customMarshalers, params Type[] types) + { + JavaScriptMarshalImpl.RegisterCustomMarshalers(customMarshalers); + var signature = JavaScriptMarshalImpl.GetMethodSignature(customMarshalers, types); + + // TODO wrap with JSObject + var exceptionMessage = JavaScriptMarshalImpl._BindJSFunction(functionName, signature.Header, out IntPtr jsFunctionHandle, out int isException); + if (isException != 0) + { + throw new JSException(exceptionMessage); + } + signature.JSHandle = jsFunctionHandle; + + return signature; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvokeBoundJSFunction(JavaScriptMarshalerSignature signature, JavaScriptMarshalerArguments data) + { + JavaScriptMarshalImpl._InvokeBoundJSFunction(signature.JSHandle, data.Buffer); + if (data.Exception.TypeHandle != IntPtr.Zero) + { + JavaScriptMarshalImpl.ThrowException(data.Exception); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe JavaScriptMarshalerArguments CreateArguments(void* buffer) + { + return new JavaScriptMarshalerArguments + { + Buffer = buffer + }; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void InitResult(ref string messageRoot, ref object root, JavaScriptMarshalerArguments args, JavaScriptMarshalerSignature signature) + { +#if DEBUG + // check alignment + Debug.Assert((int)args.Buffer % 8 == 0); +#endif + var exc = args.Exception; + exc.TypeHandle = IntPtr.Zero; + exc.RootRef = (IntPtr)Unsafe.AsPointer(ref messageRoot); + + var res = args.Result; + var sig = signature.Result; + + res.TypeHandle = sig.TypeHandle; + if (sig.BufferOffset != -1) + { + var buf = (IntPtr)args.Buffer; + res.ExtraBufferPtr = buf + signature.ArgumentsBufferLength + sig.BufferOffset; + } + if (sig.UseRoot != 0) + { + res.RootRef = (IntPtr)Unsafe.AsPointer(ref root); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void InitVoid(ref string exceptionMessageRoot, JavaScriptMarshalerArguments args) + { +#if DEBUG + // check alignment + Debug.Assert((int)args.Buffer % 8 == 0); +#endif + var exc = args.Exception; + exc.TypeHandle = IntPtr.Zero; + exc.RootRef = (IntPtr)Unsafe.AsPointer(ref exceptionMessageRoot); + + var res = args.Result; + res.TypeHandle = IntPtr.Zero; + res.RootRef = IntPtr.Zero; + res.ExtraBufferPtr = IntPtr.Zero; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void InitArgument(int position, ref T value, JavaScriptMarshalerArguments args, JavaScriptMarshalerSignature signature) + { +#if DEBUG + // check alignment + Debug.Assert((int)args.Buffer % 8 == 0); +#endif + var arg = args[position]; + var sig = signature[position]; + + arg.TypeHandle = sig.TypeHandle; + if (sig.BufferOffset != -1) + { + var buf = (IntPtr)args.Buffer; + arg.ExtraBufferPtr = buf + signature.ArgumentsBufferLength + sig.BufferOffset; + } + if (sig.UseRoot != 0) + { + arg.RootRef = (IntPtr)Unsafe.AsPointer(ref value); + } + } + + public static void SetRootRef(ref string value, JavaScriptMarshalerArg arg) + { + var valuePtrPtr = (IntPtr*)Unsafe.AsPointer(ref value); + var rootPtr = (IntPtr*)arg.RootRef.ToPointer(); + rootPtr[0] = valuePtrPtr[0]; + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshalerBase.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshalerBase.cs new file mode 100644 index 00000000000000..fcb69fc746d5c0 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/JavaScriptMarshalerBase.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.Versioning; + +namespace System.Runtime.InteropServices.JavaScript +{ + [SupportedOSPlatform("browser")] + public abstract class JavaScriptMarshalerBase + { + internal IntPtr MarshallerJSHandle; + + internal abstract Type MarshaledType { get; } + protected virtual int FixedBufferLength => 0; + protected virtual bool UseRoot => false; + + /// + /// See MarshallerFactory type: + /// toManaged : (arg: JavaScriptMarshalerArg, value: any) => void + /// toJavaScript : (arg: JavaScriptMarshalerArg) => any + /// embedded resource name format: "{Namespace}.{Folder}.{filename}.{Extension}" + /// + protected abstract string? JavaScriptCode { get; } + + // this would only get called when the call is not roslyn generated + internal abstract object ToManagedDynamic(JavaScriptMarshalerArg arg); + // this would only get called when the call is not roslyn generated + internal abstract void ToJavaScriptDynamic(object value, JavaScriptMarshalerArg arg); + + internal int GetFixedBufferLength() + { + return FixedBufferLength; + } + + internal bool GetUseRoot() + { + return UseRoot; + } + + internal string? GetJavaScriptCode() + { + return JavaScriptCode; + } + } + + [SupportedOSPlatform("browser")] + public abstract class JavaScriptMarshalerBase : JavaScriptMarshalerBase + { + internal override Type MarshaledType => typeof(T); + protected abstract MarshalToManagedDelegate ToManaged { get; } + protected abstract MarshalToJavaScriptDelegate ToJavaScript { get; } + protected virtual MarshalToJavaScriptDelegate? AfterToJavaScript => null; + //TODO protected virtual BeforeMarshalToManagedDelegate? BeforeToManaged => null; + + // this would only get called when the call is not roslyn generated + internal override object ToManagedDynamic(JavaScriptMarshalerArg arg) + { +#pragma warning disable CS8603 // Possible null reference return. + return ToManaged(arg); +#pragma warning restore CS8603 // Possible null reference return. + } + // this would only get called when the call is not roslyn generated + internal override void ToJavaScriptDynamic(object value, JavaScriptMarshalerArg arg) + { + T valueT = (T)value; + ToJavaScript(ref valueT, arg); + } + } + + //TODO public delegate void BeforeMarshalToManagedDelegate(JavaScriptMarshalerArg arg); + public delegate T MarshalToManagedDelegate(JavaScriptMarshalerArg arg); + public delegate void MarshalToJavaScriptDelegate(ref T value, JavaScriptMarshalerArg arg); + +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/NullableMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/NullableMarshaler.cs new file mode 100644 index 00000000000000..485dca5c500b55 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/NullableMarshaler.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript +{ + public sealed class NullableMarshaler : JavaScriptMarshalerBase + where T : struct + { + protected override MarshalToManagedDelegate ToManaged => MarshalToManaged; + protected override MarshalToJavaScriptDelegate ToJavaScript => MarshalToJs; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe T? MarshalToManaged(JavaScriptMarshalerArg arg) + { + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + else if (arg.TypeHandle == typeof(DateTime?).TypeHandle.Value) + { + return (T)(object)DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value).UtcDateTime; + } + else if (arg.TypeHandle == typeof(DateTimeOffset?).TypeHandle.Value) + { + return (T)(object)DateTimeOffset.FromUnixTimeMilliseconds(arg.Int64Value); + } + long v = arg.Int64Value; + return Unsafe.As(ref v); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MarshalToJs(ref T? value, JavaScriptMarshalerArg arg) + { + if (!value.HasValue) + { + arg.TypeHandle = IntPtr.Zero; + } + else if (typeof(T) == typeof(DateTime)) + { + arg.TypeHandle = JavaScriptMarshalImpl.dateTimeType; + arg.Int64Value = new DateTimeOffset(((DateTime?)(object)value).Value).ToUnixTimeMilliseconds(); + } + else if (typeof(T) == typeof(DateTimeOffset)) + { + arg.TypeHandle = JavaScriptMarshalImpl.dateTimeOffsetType; + arg.Int64Value = ((DateTimeOffset?)(object)value).Value.ToUnixTimeMilliseconds(); + } + else + { + var v = value.Value; + arg.Int64Value = Unsafe.As(ref v); + arg.TypeHandle = typeof(T?).TypeHandle.Value; + } + } + + protected override string JavaScriptCode + { + get + { + Type type = typeof(T); + if (type == typeof(bool)) + { + return tmp.Replace("XXX", "b8"); + } + if (type == typeof(byte)) + { + return tmp.Replace("XXX", "b32"); + } + if (type == typeof(short)) + { + return tmp.Replace("XXX", "i16"); + } + if (type == typeof(IntPtr) || type == typeof(int)) + { + return tmp.Replace("XXX", "i32"); + } + if (type == typeof(long)) + { + return tmp.Replace("XXX", "i64"); + } + if (type == typeof(float)) + { + return tmp.Replace("XXX", "f32"); + } + if (type == typeof(double)) + { + return tmp.Replace("XXX", "f64"); + } + if (type == typeof(DateTime) || type == typeof(DateTimeOffset)) + { + return tmp.Replace("XXX", "date"); + } + throw new NotImplementedException(); + } + } + + private const string tmp = @"function createNullableMarshaler() { + return { + toManaged: (arg, value) => { + if (value === null || value === undefined) { + set_arg_type(arg, 0); + } + else { + set_arg_type(arg, mono_type); + set_arg_XXX(arg, value); + } + }, + toJavaScript: (arg) => { + const type = get_arg_type(arg); + if (!type) { + return null; + } + return get_arg_XXX(arg); + } + }}"; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/UriMarshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/UriMarshaler.cs new file mode 100644 index 00000000000000..78690947369ff5 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Marshalers/UriMarshaler.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +#nullable disable + +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public class UriMarshaler : JavaScriptMarshalerBase + { + protected override bool UseRoot => true; + protected override MarshalToManagedDelegate ToManaged => MarshalToManaged; + protected override MarshalToJavaScriptDelegate ToJavaScript => MarshalToJs; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Uri MarshalToManaged(JavaScriptMarshalerArg arg) + { + if (arg.TypeHandle == IntPtr.Zero) + { + return null; + } + var value = Unsafe.AsRef(arg.RootRef.ToPointer()); + return new Uri(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void MarshalToJs(ref Uri value, JavaScriptMarshalerArg arg) + { + if (value == null) + { + arg.TypeHandle = IntPtr.Zero; + } + else + { + var uriValue = value.ToString(); + JavaScriptMarshal.SetRootRef(ref uriValue, arg); + } + } + + protected override string JavaScriptCode => @"function createUriMarshaller() { + return { + toManaged: (arg, value) => { + if (!value) { + set_arg_type(arg, 0); + } + else { + set_arg_type(arg, mono_type); + const pStr = BINDING.js_string_to_mono_string(value); + set_root_ref(arg, pStr); + } + }, + toJavaScript: (arg) => { + const type = get_arg_type(arg); + if (!type) { + return null; + } + const ref = get_root_ref(arg); + const value = BINDING.conv_string(ref); + return value; + } + }}"; + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArg.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArg.cs new file mode 100644 index 00000000000000..56139d1930f192 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArg.cs @@ -0,0 +1,195 @@ +// 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.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial struct JavaScriptMarshalerArg + { + // intentionaly opaque internal structure + + //TODO add to ref assembly + public unsafe IntPtr TypeHandle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].TypeHandle; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].TypeHandle = value; + } + } + + public unsafe IntPtr RootRef + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].RootRef; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].RootRef = value; + } + } + + public unsafe IntPtr JSHandle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].JSHandle; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].JSHandle = value; + } + } + + public unsafe IntPtr GCHandle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].GCHandle; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].GCHandle = value; + } + } + + public unsafe bool BooleanValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].BooleanValue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].BooleanValue = value; + } + } + + public unsafe byte ByteValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].ByteValue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].ByteValue = value; + } + } + + public unsafe short Int16Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].Int16Value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].Int16Value = value; + } + } + + public unsafe int Int32Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].Int32Value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].Int32Value = value; + } + } + + public unsafe long Int64Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].Int64Value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].Int64Value = value; + } + } + + public unsafe double DoubleValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].DoubleValue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].DoubleValue = value; + } + } + + public unsafe float SingleValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].SingleValue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].SingleValue = value; + } + } + + public unsafe IntPtr IntPtrValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].IntPtrValue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].IntPtrValue = value; + } + } + + public unsafe IntPtr ExtraBufferPtr + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return Value[0].ExtraBufferPtr; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + Value[0].ExtraBufferPtr = value; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArguments.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArguments.cs new file mode 100644 index 00000000000000..1ce55b2c4fc884 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerArguments.cs @@ -0,0 +1,55 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices.JavaScript.Private; + +namespace System.Runtime.InteropServices.JavaScript +{ + [CLSCompliant(false)] + public partial struct JavaScriptMarshalerArguments + { + // intentionaly opaque internal structure + + public unsafe JavaScriptMarshalerArg Result + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var buf = (JSMarshalerArgumentsHeader*)Buffer; + return new JavaScriptMarshalerArg + { + Value = &buf[0].Result + }; + } + } + + public unsafe JavaScriptMarshalerArg Exception + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var buf = (JSMarshalerArgumentsHeader*)Buffer; + return new JavaScriptMarshalerArg + { + Value = &buf[0].Exception + }; + } + } + + /// One based argument position + public unsafe JavaScriptMarshalerArg this[int position] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var offset = sizeof(JSMarshalerArgumentsHeader) + (position - 1) * sizeof(JSMarshalerArg); + var buf = (IntPtr)Buffer; + return new JavaScriptMarshalerArg + { + Value = (JSMarshalerArg*)(buf + offset) + }; + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerSignature.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerSignature.cs new file mode 100644 index 00000000000000..ee4384822cad1a --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Public/Structures/JavaScriptMarshalerSignature.cs @@ -0,0 +1,21 @@ +// 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.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript +{ + public partial class JavaScriptMarshalerSignature + { + // intentionaly opaque internal structure + + public unsafe int TotalBufferLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return ArgumentsBufferLength + ExtraBufferLength; + } + } + } +} 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 77614b4f7b3b1a..7b5cee7f5987b1 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 @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.InteropServices.JavaScript.Private; using System.Threading.Tasks; namespace System.Runtime.InteropServices.JavaScript @@ -285,7 +286,7 @@ internal static char GetCallSignatureCharacterForMarshalType(MarshalType t, char /// [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", Justification = "Task.Result is preserved by the ILLinker because _taskGetResultMethodInfo was initialized with it.")] - private static MethodInfo? GetTaskResultMethodInfo(Type taskType) + internal static MethodInfo? GetTaskResultMethodInfo(Type taskType) { MethodInfo? result = taskType.GetMethod(TaskGetResultName); if (result != null && result.HasSameMetadataDefinitionAs(_taskGetResultMethodInfo)) @@ -301,7 +302,7 @@ public static string ObjectToStringRef(ref object o) return o.ToString() ?? string.Empty; } - public static double GetDateValueRef(ref object dtv!!) + public static double GetDateValueRef(ref object dtv) { if (!(dtv is DateTime dt)) throw new InvalidCastException(SR.Format(SR.UnableCastObjectToType, dtv.GetType(), typeof(DateTime))); 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 871f62d5d16d7b..f8374513f4f7cb 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 @@ -3,11 +3,16 @@ true $(NetCoreAppCurrent)-Browser true - $(WasmXHarnessArgs) --engine-arg=--expose-gc --web-server-use-cop + $(WasmXHarnessArgs) --engine-arg=--expose-gc --web-server-use-cop --no-headless + + true + + + @@ -16,13 +21,22 @@ + + + + + + + + + diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs new file mode 100644 index 00000000000000..baaa721f7ec63e --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JSImportExportTest.cs @@ -0,0 +1,1114 @@ +// 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.IO; +using System.Runtime.InteropServices.JavaScript.Private; +using System.Threading.Tasks; +using Xunit; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + + // TODO js Arrays + // TODO cs Arrays + // TODO cs Span + // TODO cs IntPtr + // TODO cs void* + // TODO cs char + + public class JSImportExportTest : IAsyncLifetime + { + [Fact] + public unsafe void StructSize() + { + Assert.Equal(20, sizeof(JSMarshalerSig)); + Assert.Equal(16, sizeof(JSMarshalerArg)); + Assert.Equal(4, sizeof(JavaScriptMarshalerArg)); + Assert.Equal(4, sizeof(JavaScriptMarshalerArguments)); + } + + #region Boolean + public static IEnumerable MarshalBooleanCases() + { + yield return new object[] { true }; + yield return new object[] { false }; + } + + [Theory] + [MemberData(nameof(MarshalBooleanCases))] + public void JsImportBoolean(bool value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Boolean, + JavaScriptTestHelper.retrieve1_Boolean, + JavaScriptTestHelper.echo1_Boolean, + JavaScriptTestHelper.throw1_Boolean, + JavaScriptTestHelper.identity1_Boolean, + "boolean"); + } + + [Theory] + [MemberData(nameof(MarshalBooleanCases))] + public void JsExportBoolean(bool value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Boolean, + nameof(JavaScriptTestHelper.EchoBoolean), + "boolean"); + } + #endregion Boolean + + #region Byte + public static IEnumerable MarshalByteCases() + { + yield return new object[] { (byte)42 }; + yield return new object[] { (byte)1 }; + yield return new object[] { byte.MaxValue }; + yield return new object[] { byte.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalByteCases))] + public void JsImportByte(byte value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Byte, + JavaScriptTestHelper.retrieve1_Byte, + JavaScriptTestHelper.echo1_Byte, + JavaScriptTestHelper.throw1_Byte, + JavaScriptTestHelper.identity1_Byte, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalByteCases))] + public void JsExportByte(byte value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Byte, + nameof(JavaScriptTestHelper.EchoByte), + "number"); + } + #endregion Byte + + #region Int16 + public static IEnumerable MarshalInt16Cases() + { + yield return new object[] { 42 }; + yield return new object[] { 0 }; + yield return new object[] { 1 }; + yield return new object[] { -1 }; + yield return new object[] { short.MaxValue }; + yield return new object[] { short.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalInt16Cases))] + public void JsImportInt16(short value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Int16, + JavaScriptTestHelper.retrieve1_Int16, + JavaScriptTestHelper.echo1_Int16, + JavaScriptTestHelper.throw1_Int16, + JavaScriptTestHelper.identity1_Int16, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalInt16Cases))] + public void JsExportInt16(short value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Int16, + nameof(JavaScriptTestHelper.EchoInt16), + "number"); + } + #endregion Int16 + + #region Int32 + public static IEnumerable MarshalInt32Cases() + { + yield return new object[] { 42 }; + yield return new object[] { 0 }; + yield return new object[] { 1 }; + yield return new object[] { -1 }; + yield return new object[] { int.MaxValue }; + yield return new object[] { int.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalInt32Cases))] + public void JsImportInt32(int value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Int32, + JavaScriptTestHelper.retrieve1_Int32, + JavaScriptTestHelper.echo1_Int32, + JavaScriptTestHelper.throw1_Int32, + JavaScriptTestHelper.identity1_Int32, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalInt32Cases))] + public void JsExportInt32(int value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Int32, + nameof(JavaScriptTestHelper.EchoInt32), + "number"); + } + #endregion Int32 + + #region Int64 + public static IEnumerable MarshalInt64Cases() + { + yield return new object[] { -1 }; + yield return new object[] { 42 }; + yield return new object[] { 0 }; + yield return new object[] { 1 }; + yield return new object[] { 9007199254740991 }; // Number.MAX_SAFE_INTEGER + yield return new object[] { -9007199254740991 }; // Number.MIN_SAFE_INTEGER + } + + [Theory] + [MemberData(nameof(MarshalInt64Cases))] + public void JsImportInt64(long value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Int64, + JavaScriptTestHelper.retrieve1_Int64, + JavaScriptTestHelper.echo1_Int64, + JavaScriptTestHelper.throw1_Int64, + JavaScriptTestHelper.identity1_Int64, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalInt64Cases))] + public void JsExportInt64(long value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Int64, + nameof(JavaScriptTestHelper.EchoInt64), + "number"); + } + #endregion Int64 + + #region Double + public static IEnumerable MarshalDoubleCases() + { + yield return new object[] { Math.PI }; + yield return new object[] { 0.0 }; + yield return new object[] { double.MaxValue }; + yield return new object[] { double.MinValue }; + yield return new object[] { double.NegativeInfinity }; + yield return new object[] { double.PositiveInfinity }; + yield return new object[] { double.NaN }; + } + + [Theory] + [MemberData(nameof(MarshalDoubleCases))] + public void JsImportDouble(double value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Double, + JavaScriptTestHelper.retrieve1_Double, + JavaScriptTestHelper.echo1_Double, + JavaScriptTestHelper.throw1_Double, + JavaScriptTestHelper.identity1_Double, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalDoubleCases))] + public void JsExportDouble(double value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Double, + nameof(JavaScriptTestHelper.EchoDouble), + "number"); + } + #endregion Double + + #region Single + public static IEnumerable MarshalSingleCases() + { + yield return new object[] { (float)Math.PI }; + yield return new object[] { 0.0f }; + yield return new object[] { float.MaxValue }; + yield return new object[] { float.MinValue }; + yield return new object[] { float.NegativeInfinity }; + yield return new object[] { float.PositiveInfinity }; + yield return new object[] { float.NaN }; + } + + [Theory] + [MemberData(nameof(MarshalSingleCases))] + public void JsImportSingle(float value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Single, + JavaScriptTestHelper.retrieve1_Single, + JavaScriptTestHelper.echo1_Single, + JavaScriptTestHelper.throw1_Single, + JavaScriptTestHelper.identity1_Single, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalSingleCases))] + public void JsExportSingle(float value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Single, + nameof(JavaScriptTestHelper.EchoSingle), + "number"); + } + #endregion Single + + #region IntPtr + public static IEnumerable MarshalIntPtrCases() + { + yield return new object[] { (IntPtr)42 }; + yield return new object[] { IntPtr.Zero }; + yield return new object[] { (IntPtr)1 }; + yield return new object[] { (IntPtr)(-1) }; + yield return new object[] { IntPtr.MaxValue }; + yield return new object[] { IntPtr.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalIntPtrCases))] + public void JsImportIntPtr(IntPtr value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_IntPtr, + JavaScriptTestHelper.retrieve1_IntPtr, + JavaScriptTestHelper.echo1_IntPtr, + JavaScriptTestHelper.throw1_IntPtr, + JavaScriptTestHelper.identity1_IntPtr, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalIntPtrCases))] + public void JsExportIntPtr(IntPtr value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_IntPtr, + nameof(JavaScriptTestHelper.EchoIntPtr), + "number"); + } + #endregion IntPtr + + #region Datetime + public static IEnumerable MarshalDateTimeCases() + { + yield return new object[] { new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) }; + yield return new object[] { TrimNano(DateTime.UtcNow) }; + yield return new object[] { TrimNano(DateTime.MaxValue) }; + } + + [Theory] + [MemberData(nameof(MarshalDateTimeCases))] + public void JSImportDateTime(DateTime value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_DateTime, + JavaScriptTestHelper.retrieve1_DateTime, + JavaScriptTestHelper.echo1_DateTime, + JavaScriptTestHelper.throw1_DateTime, + JavaScriptTestHelper.identity1_DateTime, + "object", "Date"); + } + + [Theory] + [MemberData(nameof(MarshalDateTimeCases))] + public void JsExportDateTime(DateTime value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_DateTime, + nameof(JavaScriptTestHelper.EchoDateTime), + "object", "Date"); + } + #endregion Datetime + + #region DateTimeOffset + public static IEnumerable MarshalDateTimeOffsetCases() + { + yield return new object[] { DateTimeOffset.FromUnixTimeSeconds(0) }; + yield return new object[] { TrimNano(DateTimeOffset.UtcNow) }; + yield return new object[] { TrimNano(DateTimeOffset.MaxValue) }; + } + + [Theory] + [MemberData(nameof(MarshalDateTimeOffsetCases))] + public void JSImportDateTimeOffset(DateTimeOffset value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_DateTimeOffset, + JavaScriptTestHelper.retrieve1_DateTimeOffset, + JavaScriptTestHelper.echo1_DateTimeOffset, + JavaScriptTestHelper.throw1_DateTimeOffset, + JavaScriptTestHelper.identity1_DateTimeOffset, + "object", "Date"); + } + + [Theory] + [MemberData(nameof(MarshalDateTimeOffsetCases))] + public void JsExportDateTimeOffset(DateTimeOffset value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_DateTimeOffset, + nameof(JavaScriptTestHelper.EchoDateTimeOffset), + "object", "Date"); + } + #endregion DateTimeOffset + + #region NullableBoolean + public static IEnumerable MarshalNullableBooleanCases() + { + yield return new object[] { null }; + yield return new object[] { true }; + yield return new object[] { false }; + } + + [Theory] + [MemberData(nameof(MarshalNullableBooleanCases))] + public void JsImportNullableBoolean(bool? value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_NullableBoolean, + JavaScriptTestHelper.retrieve1_NullableBoolean, + JavaScriptTestHelper.echo1_NullableBoolean, + JavaScriptTestHelper.throw1_NullableBoolean, + JavaScriptTestHelper.identity1_NullableBoolean, + "boolean"); + } + + [Theory] + [MemberData(nameof(MarshalNullableBooleanCases))] + public void JsExportNullableBoolean(bool? value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_NullableBoolean, + nameof(JavaScriptTestHelper.EchoNullableBoolean), + "boolean"); + } + #endregion NullableBoolean + + #region NullableInt32 + public static IEnumerable MarshalNullableInt32Cases() + { + yield return new object[] { null }; + yield return new object[] { 42 }; + yield return new object[] { 0 }; + yield return new object[] { 1 }; + yield return new object[] { -1 }; + yield return new object[] { int.MaxValue }; + yield return new object[] { int.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalNullableInt32Cases))] + public void JsImportNullableInt32(int? value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_NullableInt32, + JavaScriptTestHelper.retrieve1_NullableInt32, + JavaScriptTestHelper.echo1_NullableInt32, + JavaScriptTestHelper.throw1_NullableInt32, + JavaScriptTestHelper.identity1_NullableInt32, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalNullableInt32Cases))] + public void JsExportNullableInt32(int? value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_NullableInt32, + nameof(JavaScriptTestHelper.EchoNullableInt32), + "number"); + } + #endregion NullableInt32 + + #region NullableIntPtr + public static IEnumerable MarshalNullableIntPtrCases() + { + yield return new object[] { null }; + yield return new object[] { (IntPtr)42 }; + yield return new object[] { IntPtr.Zero }; + yield return new object[] { (IntPtr)1 }; + yield return new object[] { (IntPtr)(-1) }; + yield return new object[] { IntPtr.MaxValue }; + yield return new object[] { IntPtr.MinValue }; + } + + [Theory] + [MemberData(nameof(MarshalNullableIntPtrCases))] + public void JsImportNullableIntPtr(IntPtr? value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_NullableIntPtr, + JavaScriptTestHelper.retrieve1_NullableIntPtr, + JavaScriptTestHelper.echo1_NullableIntPtr, + JavaScriptTestHelper.throw1_NullableIntPtr, + JavaScriptTestHelper.identity1_NullableIntPtr, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalNullableIntPtrCases))] + public void JsExportNullableIntPtr(IntPtr? value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_NullableIntPtr, + nameof(JavaScriptTestHelper.EchoNullableIntPtr), + "number"); + } + #endregion NullableIntPtr + + #region NullableDouble + public static IEnumerable MarshalNullableDoubleCases() + { + yield return new object[] { null }; + yield return new object[] { Math.PI }; + yield return new object[] { 0.0 }; + yield return new object[] { double.MaxValue }; + yield return new object[] { double.MinValue }; + yield return new object[] { double.NegativeInfinity }; + yield return new object[] { double.PositiveInfinity }; + yield return new object[] { double.NaN }; + } + + [Theory] + [MemberData(nameof(MarshalNullableDoubleCases))] + public void JsImportNullableDouble(double? value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_NullableDouble, + JavaScriptTestHelper.retrieve1_NullableDouble, + JavaScriptTestHelper.echo1_NullableDouble, + JavaScriptTestHelper.throw1_NullableDouble, + JavaScriptTestHelper.identity1_NullableDouble, + "number"); + } + + [Theory] + [MemberData(nameof(MarshalNullableDoubleCases))] + public void JsExportNullableDouble(double? value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_NullableDouble, + nameof(JavaScriptTestHelper.EchoNullableDouble), + "number"); + } + #endregion NullableDouble + + #region NullableDateTime + public static IEnumerable MarshalNullableDateTimeCases() + { + yield return new object[] { null }; + yield return new object[] { new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) }; + yield return new object[] { TrimNano(DateTime.UtcNow) }; + yield return new object[] { TrimNano(DateTime.MaxValue) }; + } + + [Theory] + [MemberData(nameof(MarshalNullableDateTimeCases))] + public void JsImportNullableDateTime(DateTime? value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_NullableDateTime, + JavaScriptTestHelper.retrieve1_NullableDateTime, + JavaScriptTestHelper.echo1_NullableDateTime, + JavaScriptTestHelper.throw1_NullableDateTime, + JavaScriptTestHelper.identity1_NullableDateTime, + "object"); + } + + [Theory] + [MemberData(nameof(MarshalNullableDateTimeCases))] + public void JsExportNullableDateTime(DateTime? value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_NullableDateTime, + nameof(JavaScriptTestHelper.EchoNullableDateTime), + "object"); + } + #endregion NullableDateTime + + #region String + public static IEnumerable MarshalStringCases() + { + yield return new object[] { "Hello-" + Random.Shared.Next() + "-JS" }; + //yield return new object[] { string.Intern("Hello JS Interned!") }; + //yield return new object[] { (string)null }; + } + + [Theory] + [MemberData(nameof(MarshalStringCases))] + public void JsImportString(string value) + { + JsImportTest(value, + JavaScriptTestHelper.store1_String, + JavaScriptTestHelper.retrieve1_String, + JavaScriptTestHelper.echo1_String, + JavaScriptTestHelper.throw1_String, + JavaScriptTestHelper.identity1_String + , "string"); + } + + [Theory] + [MemberData(nameof(MarshalStringCases))] + public void JsExportString(string value) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_String, + nameof(JavaScriptTestHelper.EchoString), + "string"); + } + #endregion String + + #region Object + public static IEnumerable MarshalObjectCases() + { + yield return new object[] { new object(), "ManagedObject" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalObjectCases))] + public void JSImportObject(object value, string clazz) + { + JsImportTest(value, + JavaScriptTestHelper.store1_Object, + JavaScriptTestHelper.retrieve1_Object, + JavaScriptTestHelper.echo1_Object, + JavaScriptTestHelper.throw1_Object, + JavaScriptTestHelper.identity1_Object, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalObjectCases))] + public void JsExportObject(object value, string clazz) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_Object, + nameof(JavaScriptTestHelper.EchoObject), + "object", clazz); + } + #endregion Object + + #region List + public static IEnumerable MarshalListCases() + { + yield return new object[] { new List(), "ManagedObject" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalListCases))] + public void JSImportList(List value, string clazz) + { + JsImportTest(value, + JavaScriptTestHelper.store1_ListOfInt, + JavaScriptTestHelper.retrieve1_ListOfInt, + JavaScriptTestHelper.echo1_ListOfInt, + JavaScriptTestHelper.throw1_ListOfInt, + JavaScriptTestHelper.identity1_ListOfInt, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalListCases))] + public void JsExportList(List value, string clazz) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_ListOfInt, + nameof(JavaScriptTestHelper.EchoListOfInt), + "object", clazz); + } + #endregion List + + #region Exception + public static IEnumerable MarshalExceptionCases() + { + yield return new object[] { new Exception("Test"), "ManagedError" }; + yield return new object[] { null, "JSTestError" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalExceptionCases))] + public void JSImportException(Exception value, string clazz) + { + if (clazz == "JSTestError") + { + value = JavaScriptTestHelper.createException("!CreateEx!"); + } + + JsImportTest(value, + JavaScriptTestHelper.store1_Exception, + JavaScriptTestHelper.retrieve1_Exception, + JavaScriptTestHelper.echo1_Exception, + JavaScriptTestHelper.throw1_Exception, + JavaScriptTestHelper.identity1_Exception, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalExceptionCases))] + public void JsExportException(Exception value, string clazz) + { + if (clazz == "JSTestError") + { + value = JavaScriptTestHelper.createException("!CreateEx!"); + } + + JsExportTest(value, + JavaScriptTestHelper.invoke1_Exception, + nameof(JavaScriptTestHelper.EchoException), + "object", clazz); + } + #endregion Exception + + #region JSException + public static IEnumerable MarshalJSExceptionCases() + { + yield return new object[] { new JSException("Test"), "ManagedError" };// marshaled as ManagedError because it doesn't have the js_handle + yield return new object[] { null, "JSTestError" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalJSExceptionCases))] + public void JSImportJSException(JSException value, string clazz) + { + if (clazz == "JSTestError") + { + value = JavaScriptTestHelper.createException("!CreateEx!"); + } + JsImportTest(value, + JavaScriptTestHelper.store1_JSException, + JavaScriptTestHelper.retrieve1_JSException, + JavaScriptTestHelper.echo1_JSException, + JavaScriptTestHelper.throw1_JSException, + JavaScriptTestHelper.identity1_JSException, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalJSExceptionCases))] + public void JsExportJSException(JSException value, string clazz) + { + if (clazz == "JSTestError") + { + value = JavaScriptTestHelper.createException("!CreateEx!"); + } + + JsExportTest(value, + JavaScriptTestHelper.invoke1_JSException, + nameof(JavaScriptTestHelper.EchoJSException), + "object", clazz); + } + #endregion JSException + + #region IOException + public static IEnumerable MarshalIOExceptionCases() + { + yield return new object[] { new IOException("Test"), "ManagedError" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalIOExceptionCases))] + public void JSImportIOException(IOException value, string clazz) + { + JsImportTest(value, + JavaScriptTestHelper.store1_IOException, + JavaScriptTestHelper.retrieve1_IOException, + JavaScriptTestHelper.echo1_IOException, + JavaScriptTestHelper.throw1_IOException, + JavaScriptTestHelper.identity1_IOException, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalIOExceptionCases))] + public void JsExportIOException(IOException value, string clazz) + { + JsExportTest(value, + JavaScriptTestHelper.invoke1_IOException, + nameof(JavaScriptTestHelper.EchoIOException), + "object", clazz); + } + #endregion IOException + + #region IJSObject + public static IEnumerable MarshalIJSObjectCases() + { + yield return new object[] { null, "JSData" }; + yield return new object[] { null, null }; + } + + [Theory] + [MemberData(nameof(MarshalIJSObjectCases))] + public void JSImportIJSObject(IJSObject value, string clazz) + { + if (clazz == "JSData") + { + value = JavaScriptTestHelper.createData("!CreateJS!"); + } + + JsImportTest(value, + JavaScriptTestHelper.store1_IJSObject, + JavaScriptTestHelper.retrieve1_IJSObject, + JavaScriptTestHelper.echo1_IJSObject, + JavaScriptTestHelper.throw1_IJSObject, + JavaScriptTestHelper.identity1_IJSObject, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalIJSObjectCases))] + public void JsExportIJSObject(IJSObject value, string clazz) + { + if (clazz == "JSData") + { + value = JavaScriptTestHelper.createData("!CreateJS!"); + } + + JsExportTest(value, + JavaScriptTestHelper.invoke1_IJSObject, + nameof(JavaScriptTestHelper.EchoIJSObject), + "object", clazz); + } + #endregion IJSObject + + #region JSObject + [Theory] + [MemberData(nameof(MarshalIJSObjectCases))] + public void JSImportJSObject(JSObject value, string clazz) + { + if (clazz == "JSData") + { + value = (JSObject)JavaScriptTestHelper.createData("!CreateJS!"); + } + + JsImportTest(value, + JavaScriptTestHelper.store1_JSObject, + JavaScriptTestHelper.retrieve1_JSObject, + JavaScriptTestHelper.echo1_JSObject, + JavaScriptTestHelper.throw1_JSObject, + JavaScriptTestHelper.identity1_JSObject, + "object", clazz); + } + + [Theory] + [MemberData(nameof(MarshalIJSObjectCases))] + public void JsExportJSObject(JSObject value, string clazz) + { + if (clazz == "JSData") + { + value = (JSObject)JavaScriptTestHelper.createData("!CreateJS!"); + } + + JsExportTest(value, + JavaScriptTestHelper.invoke1_JSObject, + nameof(JavaScriptTestHelper.EchoJSObject), + "object", clazz); + } + #endregion JSObject + + #region ProxyOfProxy + [Fact] + public void ProxyOfProxyThrows() + { + // proxy of proxy should throw + JavaScriptTestHelper.store1_Object(new object()); + Assert.Throws(() => JavaScriptTestHelper.retrieve1_JSObject()); + } + + + [Fact] + public void ProxyOfIntThrows() + { + // JSObject proxy of int should throw + JavaScriptTestHelper.store1_Int32(13); + Assert.Throws(() => JavaScriptTestHelper.retrieve1_JSObject()); + } + #endregion + + #region Task + + [Fact] + public async Task JsImportTaskEchoComplete() + { + var task = JavaScriptTestHelper.echo1_Task(Task.CompletedTask); + Assert.NotEqual(Task.CompletedTask, task); + // yield to main loop, because "the respective handler function will be called asynchronously" + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#return_value + await Task.Delay(100); + Assert.True(task.IsCompleted); + } + + [Fact] + public async Task JsImportTaskEchoPendingResult() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + var task = JavaScriptTestHelper.echo1_Task(tcs.Task); + Assert.NotEqual(tcs.Task, task); + Assert.False(task.IsCompleted); + + tcs.SetResult("test"); + // yield to main loop, because "the respective handler function will be called asynchronously" + await Task.Delay(100); + Assert.True(task.IsCompleted); + Assert.Equal("test", ((Task)task).Result); + } + + [Fact] + public async Task JsImportTaskEchoPendingException() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + var task = JavaScriptTestHelper.echo1_Task(tcs.Task); + Assert.NotEqual(tcs.Task, task); + Assert.False(task.IsCompleted); + + tcs.SetException(new Exception("Test")); + // yield to main loop, because "the respective handler function will be called asynchronously" + await Task.Delay(100); + Assert.True(task.IsFaulted); + await Assert.ThrowsAsync(async () => await task); + } + + public static IEnumerable TaskCases() + { + yield return new object[] { Math.PI }; + yield return new object[] { 0 }; + yield return new object[] { "test" }; + yield return new object[] { null }; + } + + [Theory] + [MemberData(nameof(TaskCases))] + public async Task JsImportTaskAwaitPendingResult(object result) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + var task = JavaScriptTestHelper.await1(tcs.Task); + Assert.NotEqual(tcs.Task, task); + Assert.False(task.IsCompleted); + + tcs.SetResult(result); + // yield to main loop, because "the respective handler function will be called asynchronously" + await Task.Delay(100); + Assert.True(task.IsCompleted); + var res = await task; + if (result != null && result.GetType() == typeof(int)) + { + Assert.Equal(result, Convert.ToInt32(res)); + } + else + { + Assert.Equal(result, res); + } + } + + [Fact] + public async Task JsImportTaskAwaitPendingException() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + var task = JavaScriptTestHelper.await1(tcs.Task); + Assert.NotEqual(tcs.Task, task); + Assert.False(task.IsCompleted); + + tcs.SetException(new Exception("Test")); + // yield to main loop, because "the respective handler function will be called asynchronously" + await Task.Delay(100); + Assert.True(task.IsFaulted); + await Assert.ThrowsAsync(async () => await task); + } + + + [Fact] + public async Task JsImportTaskAwait() + { + var task = JavaScriptTestHelper.awaitvoid(Task.CompletedTask); + await Task.Delay(100); + Assert.True(task.IsCompleted); + await task; + } + + /*[Theory] + [MemberData(nameof(MarshalInt32Cases))] + public async Task JsExportTaskOfInt(int value) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + + var res = JavaScriptTestHelper.invoke1_TaskOfObject(tcs.Task, nameof(JavaScriptTestHelper.AwaitTaskOfObject)); + JavaScriptTestHelper.Log("JsExportTaskOfInt A"); + tcs.SetResult(value); + JavaScriptTestHelper.Log("JsExportTaskOfInt B"+ res.IsCompleted); + await Task.Yield(); + var rr = await res; + await Task.Yield(); + JavaScriptTestHelper.Log("JsExportTaskOfInt C"); + Assert.Equal(value, rr); + }*/ + + #endregion + + private void JsExportTest(T value + , Func invoke, string echoName, string jsType, string? jsClass = null) + { + T res; + res = invoke(value, echoName); + Assert.Equal(value, res); + } + + private void JsImportTest(T value + , Action store1 + , Func retrieve1 + , Func echo1 + , Func throw1 + , Func identity1 + , string jsType, string? jsClass = null) + { + if (value == null) + { + jsClass = null; + jsType = "object"; + } + + // invoke + store1(value); + var res = retrieve1(); + Assert.Equal(value, res); + res = echo1(value); + Assert.Equal(value, res); + + var equals = identity1(value); + Assert.True(equals, "value not equals"); + + var actualJsType = JavaScriptTestHelper.getType1(); + Assert.Equal(jsType, actualJsType); + + if (jsClass != null) + { + var actualJsClass = JavaScriptTestHelper.getClass1(); + Assert.Equal(jsClass, actualJsClass); + } + var exThrow0 = Assert.Throws(() => JavaScriptTestHelper.throw0()); + Assert.Contains("throw-0-msg", exThrow0.Message); + Assert.DoesNotContain(" at ", exThrow0.Message); + Assert.Contains(" at throw0", exThrow0.StackTrace); + + var exThrow1 = Assert.Throws(() => throw1(value)); + Assert.Contains("throw1-msg", exThrow1.Message); + Assert.DoesNotContain(" at ", exThrow1.Message); + Assert.Contains(" at throw1", exThrow1.StackTrace); + + // anything is a system.object, sometimes it would be JSObject wrapper + if (typeof(T).IsPrimitive) + { + var resBoxed = JavaScriptTestHelper.echo1_Object(value); + // js Number always boxes as double + if(typeof(T) == typeof(IntPtr)) + { + Assert.Equal((IntPtr)(object)value, (IntPtr)(int)(double)resBoxed); + } + else if (typeof(T) == typeof(bool)) + { + Assert.Equal((bool)(object)value, (bool)resBoxed); + } + else + { + Assert.Equal(Convert.ToDouble(value), resBoxed); + } + + //TODO var task = JavaScriptTestHelper.await1(Task.FromResult((object)value)); + } + else if (typeof(T)==typeof(DateTime)) + { + var resBoxed = JavaScriptTestHelper.echo1_Object(value); + Assert.Equal(value, resBoxed); + } + else if (typeof(T)==typeof(DateTimeOffset)) + { + var resBoxed = JavaScriptTestHelper.echo1_Object(value); + Assert.Equal(((DateTimeOffset)(object)value).UtcDateTime, resBoxed); + } + else if (Nullable.GetUnderlyingType(typeof(T)) != null) + { + var resBoxed = JavaScriptTestHelper.echo1_Object(value); + if (resBoxed != null) + { + var vt = Nullable.GetUnderlyingType(typeof(T)); + if (vt == typeof(bool)) + { + Assert.Equal(((bool?)(object)value).Value, (bool)resBoxed); + } + else if (vt == typeof(DateTime)) + { + Assert.Equal(((DateTime?)(object)value).Value, resBoxed); + } + else if (vt == typeof(DateTimeOffset)) + { + Assert.Equal(((DateTimeOffset?)(object)value).Value.UtcDateTime, resBoxed); + } + else if (vt == typeof(IntPtr)) + { + Assert.Equal((double)((IntPtr?)(object)value).Value, resBoxed); + } + else + { + Assert.Equal(Convert.ToDouble(value), resBoxed); + } + } + else + { + Assert.Equal(value, default(T)); + } + } + else + { + var resObj = JavaScriptTestHelper.retrieve1_Object(); + if (resObj == null || resObj.GetType() != typeof(JSObject)) + { + Assert.Equal(value, resObj); + } + } + + if (typeof(Exception).IsAssignableFrom(typeof(T))) + { + // all exceptions are Exception + var resEx = JavaScriptTestHelper.retrieve1_Exception(); + Assert.Equal((Exception)(object)value, resEx); + } + } + + public async Task InitializeAsync() + { + await JavaScriptTestHelper.InitializeAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + // js Date doesn't have nanosecond precision + public static DateTime TrimNano(DateTime date) + { + return new DateTime(date.Ticks - (date.Ticks % TimeSpan.TicksPerMillisecond), DateTimeKind.Utc); + } + + public static DateTimeOffset TrimNano(DateTimeOffset date) + { + return new DateTime(date.Ticks - (date.Ticks % TimeSpan.TicksPerMillisecond), DateTimeKind.Utc); + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs new file mode 100644 index 00000000000000..024d2b57babaae --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.cs @@ -0,0 +1,535 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + public partial class JavaScriptTestHelper + { + [JSImport("console.log")] + public static partial void Log(string message); + + [JSImport("javaScriptTestHelper.getType1")] + internal static partial string getType1(); + + [JSImport("javaScriptTestHelper.getClass1")] + internal static partial string getClass1(); + + [JSImport("javaScriptTestHelper.throw0")] + internal static partial void throw0(); + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial Task echo1_Task(Task arg1); + + [JSImport("javaScriptTestHelper.createException")] + internal static partial JSException createException(string name); + + [JSImport("javaScriptTestHelper.createData")] + internal static partial IJSObject createData(string name); + + #region Task + [JSImport("javaScriptTestHelper.awaitvoid")] + internal static partial Task awaitvoid(Task arg1); + [JSImport("javaScriptTestHelper.await1")] + internal static partial Task await1(Task arg1); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial Task invoke1_TaskOfObject(Task value, string name); + [JSExport("JavaScriptTestHelper.AwaitTaskOfObject")] + public static async Task AwaitTaskOfObject(Task arg1) + { + var res = await arg1; + return res; + } + #endregion + + #region Boolean + [JSImport("javaScriptTestHelper.echo1")] + internal static partial bool echo1_Boolean(bool value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Boolean(bool value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial bool retrieve1_Boolean(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Boolean(bool value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial bool throw1_Boolean(bool value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial bool invoke1_Boolean(bool value, string name); + [JSExport("JavaScriptTestHelper.EchoBoolean")] + public static bool EchoBoolean(bool arg1) + { + return arg1; + } + #endregion Boolean + + #region Byte + [JSImport("javaScriptTestHelper.echo1")] + internal static partial byte echo1_Byte(byte value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Byte(byte value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial byte retrieve1_Byte(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Byte(byte value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial byte throw1_Byte(byte value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial byte invoke1_Byte(byte value, string name); + [JSExport("JavaScriptTestHelper.EchoByte")] + public static byte EchoByte(byte arg1) + { + return arg1; + } + #endregion Byte + + #region Int16 + [JSImport("javaScriptTestHelper.echo1")] + internal static partial short echo1_Int16(short value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Int16(short value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial short retrieve1_Int16(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Int16(short value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial short throw1_Int16(short value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial short invoke1_Int16(short value, string name); + [JSExport("JavaScriptTestHelper.EchoInt16")] + public static short EchoInt16(short arg1) + { + return arg1; + } + #endregion Int16 + + #region Int32 + [JSImport("javaScriptTestHelper.echo1")] + internal static partial int echo1_Int32(int value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Int32(int value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial int retrieve1_Int32(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Int32(int value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial int throw1_Int32(int value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial int invoke1_Int32(int value, string name); + [JSExport("JavaScriptTestHelper.EchoInt32")] + public static int EchoInt32(int arg1) + { + return arg1; + } + #endregion Int32 + + #region Int64 + [JSImport("javaScriptTestHelper.echo1")] + internal static partial long echo1_Int64(long value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Int64(long value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial long retrieve1_Int64(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Int64(long value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial long throw1_Int64(long value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial long invoke1_Int64(long value, string name); + [JSExport("JavaScriptTestHelper.EchoInt64")] + public static long EchoInt64(long arg1) + { + return arg1; + } + #endregion Int64 + + #region Double + [JSImport("javaScriptTestHelper.echo1")] + internal static partial double echo1_Double(double value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Double(double value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial double retrieve1_Double(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Double(double value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial double throw1_Double(double value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial double invoke1_Double(double value, string name); + [JSExport("JavaScriptTestHelper.EchoDouble")] + public static double EchoDouble(double arg1) + { + return arg1; + } + #endregion Double + + #region Single + [JSImport("javaScriptTestHelper.echo1")] + internal static partial float echo1_Single(float value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Single(float value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial float retrieve1_Single(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Single(float value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial float throw1_Single(float value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial float invoke1_Single(float value, string name); + [JSExport("JavaScriptTestHelper.EchoSingle")] + public static float EchoSingle(float arg1) + { + return arg1; + } + #endregion Single + + #region IntPtr + [JSImport("javaScriptTestHelper.echo1")] + internal static partial IntPtr echo1_IntPtr(IntPtr value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_IntPtr(IntPtr value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial IntPtr retrieve1_IntPtr(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_IntPtr(IntPtr value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial IntPtr throw1_IntPtr(IntPtr value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial IntPtr invoke1_IntPtr(IntPtr value, string name); + [JSExport("JavaScriptTestHelper.EchoIntPtr")] + public static IntPtr EchoIntPtr(IntPtr arg1) + { + return arg1; + } + #endregion IntPtr + + #region DateTime + [JSImport("javaScriptTestHelper.echo1")] + internal static partial DateTime echo1_DateTime(DateTime value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_DateTime(DateTime value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial DateTime retrieve1_DateTime(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_DateTime(DateTime value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial DateTime throw1_DateTime(DateTime value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial DateTime invoke1_DateTime(DateTime value, string name); + [JSExport("JavaScriptTestHelper.EchoDateTime")] + public static DateTime EchoDateTime(DateTime arg1) + { + return arg1; + } + #endregion DateTime + + #region DateTimeOffset + [JSImport("javaScriptTestHelper.echo1")] + internal static partial DateTimeOffset echo1_DateTimeOffset(DateTimeOffset value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_DateTimeOffset(DateTimeOffset value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial DateTimeOffset retrieve1_DateTimeOffset(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_DateTimeOffset(DateTimeOffset value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial DateTimeOffset throw1_DateTimeOffset(DateTimeOffset value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial DateTimeOffset invoke1_DateTimeOffset(DateTimeOffset value, string name); + [JSExport("JavaScriptTestHelper.EchoDateTimeOffset")] + public static DateTimeOffset EchoDateTimeOffset(DateTimeOffset arg1) + { + return arg1; + } + #endregion DateTimeOffset + + #region NullableBoolean + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial bool? echo1_NullableBoolean(bool? value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_NullableBoolean(bool? value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial bool? retrieve1_NullableBoolean(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_NullableBoolean(bool? value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial bool? throw1_NullableBoolean(bool? value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial bool? invoke1_NullableBoolean(bool? value, string name); + [JSExport("JavaScriptTestHelper.EchoNullableBoolean")] + public static bool? EchoNullableBoolean(bool? arg1) + { + return arg1; + } + #endregion NullableBoolean + + #region NullableInt32 + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial int? echo1_NullableInt32(int? value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_NullableInt32(int? value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial int? retrieve1_NullableInt32(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_NullableInt32(int? value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial int? throw1_NullableInt32(int? value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial int? invoke1_NullableInt32(int? value, string name); + [JSExport("JavaScriptTestHelper.EchoNullableInt32")] + public static int? EchoNullableInt32(int? arg1) + { + return arg1; + } + #endregion NullableInt32 + + #region NullableIntPtr + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial IntPtr? echo1_NullableIntPtr(IntPtr? value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_NullableIntPtr(IntPtr? value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial IntPtr? retrieve1_NullableIntPtr(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_NullableIntPtr(IntPtr? value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial IntPtr? throw1_NullableIntPtr(IntPtr? value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial IntPtr? invoke1_NullableIntPtr(IntPtr? value, string name); + [JSExport("JavaScriptTestHelper.EchoNullableIntPtr")] + public static IntPtr? EchoNullableIntPtr(IntPtr? arg1) + { + return arg1; + } + #endregion NullableIntPtr + + #region NullableDouble + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial double? echo1_NullableDouble(double? value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_NullableDouble(double? value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial double? retrieve1_NullableDouble(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_NullableDouble(double? value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial double? throw1_NullableDouble(double? value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial double? invoke1_NullableDouble(double? value, string name); + [JSExport("JavaScriptTestHelper.EchoNullableDouble")] + public static double? EchoNullableDouble(double? arg1) + { + return arg1; + } + #endregion NullableDouble + + #region NullableDateTime + + [JSImport("javaScriptTestHelper.echo1")] + internal static partial DateTime? echo1_NullableDateTime(DateTime? value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_NullableDateTime(DateTime? value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial DateTime? retrieve1_NullableDateTime(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_NullableDateTime(DateTime? value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial DateTime? throw1_NullableDateTime(DateTime? value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial DateTime? invoke1_NullableDateTime(DateTime? value, string name); + [JSExport("JavaScriptTestHelper.EchoNullableDateTime")] + public static DateTime? EchoNullableDateTime(DateTime? arg1) + { + return arg1; + } + #endregion NullableDateTime + + #region String + [JSImport("javaScriptTestHelper.echo1")] + internal static partial string echo1_String(string value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_String(string value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial string retrieve1_String(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_String(string value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial string throw1_String(string value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial string invoke1_String(string value, string name); + [JSExport("JavaScriptTestHelper.EchoString")] + public static string EchoString(string arg1) + { + return arg1; + } + #endregion String + + #region Object + [JSImport("javaScriptTestHelper.echo1")] + internal static partial object echo1_Object(object value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Object(object value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial object retrieve1_Object(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Object(object value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial object throw1_Object(object value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial object invoke1_Object(object value, string name); + [JSExport("JavaScriptTestHelper.EchoObject")] + public static object EchoObject(object arg1) + { + return arg1; + } + #endregion Object + + #region Exception + [JSImport("javaScriptTestHelper.echo1")] + internal static partial Exception echo1_Exception(Exception value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_Exception(Exception value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial Exception retrieve1_Exception(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_Exception(Exception value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial Exception throw1_Exception(Exception value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial Exception invoke1_Exception(Exception value, string name); + [JSExport("JavaScriptTestHelper.EchoException")] + public static Exception EchoException(Exception arg1) + { + return arg1; + } + #endregion Exception + + #region IOException + [JSImport("javaScriptTestHelper.echo1")] + internal static partial IOException echo1_IOException(IOException value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_IOException(IOException value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial IOException retrieve1_IOException(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_IOException(IOException value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial IOException throw1_IOException(IOException value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial IOException invoke1_IOException(IOException value, string name); + [JSExport("JavaScriptTestHelper.EchoIOException")] + public static IOException EchoIOException(IOException arg1) + { + return arg1; + } + #endregion IOException + + #region JSException + [JSImport("javaScriptTestHelper.echo1")] + internal static partial JSException echo1_JSException(JSException value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_JSException(JSException value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial JSException retrieve1_JSException(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_JSException(JSException value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial JSException throw1_JSException(JSException value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial JSException invoke1_JSException(JSException value, string name); + [JSExport("JavaScriptTestHelper.EchoJSException")] + public static JSException EchoJSException(JSException arg1) + { + return arg1; + } + #endregion JSException + + #region JSObject + [JSImport("javaScriptTestHelper.echo1")] + internal static partial JSObject echo1_JSObject(JSObject value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_JSObject(JSObject value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial JSObject retrieve1_JSObject(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_JSObject(JSObject value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial JSObject throw1_JSObject(JSObject value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial JSObject invoke1_JSObject(JSObject value, string name); + [JSExport("JavaScriptTestHelper.EchoJSObject")] + public static JSObject EchoJSObject(JSObject arg1) + { + return arg1; + } + #endregion JSObject + + #region IJSObject + [JSImport("javaScriptTestHelper.echo1")] + internal static partial IJSObject echo1_IJSObject(IJSObject value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_IJSObject(IJSObject value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial IJSObject retrieve1_IJSObject(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_IJSObject(IJSObject value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial IJSObject throw1_IJSObject(IJSObject value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial IJSObject invoke1_IJSObject(IJSObject value, string name); + [JSExport("JavaScriptTestHelper.EchoIJSObject")] + public static IJSObject EchoIJSObject(IJSObject arg1) + { + return arg1; + } + #endregion IJSObject + + #region ListOfInt + [JSImport("javaScriptTestHelper.echo1")] + internal static partial List echo1_ListOfInt(List value); + [JSImport("javaScriptTestHelper.store1")] + internal static partial void store1_ListOfInt(List value); + [JSImport("javaScriptTestHelper.retrieve1")] + internal static partial List retrieve1_ListOfInt(); + [JSImport("javaScriptTestHelper.identity1")] + internal static partial bool identity1_ListOfInt(List value); + [JSImport("javaScriptTestHelper.throw1")] + internal static partial List throw1_ListOfInt(List value); + [JSImport("javaScriptTestHelper.invoke1")] + internal static partial List invoke1_ListOfInt(List value, string name); + [JSExport("JavaScriptTestHelper.EchoListOfInt")] + public static List EchoListOfInt(List arg1) + { + return arg1; + } + #endregion ListOfInt + + static JSObject _invokeJsTester; + public static async Task InitializeAsync() + { + if (_invokeJsTester == null) + { + Function helper = new Function(@" + const loadJs = async () => { + try{ + await import('./JavaScriptTestHelper.js'); + console.log('LOADED JavaScriptTestHelper.js'); + } + catch(ex){ + console.log('FAILED loading JavaScriptTestHelper.js ' + ex); + } + }; + return loadJs(); + "); + await (Task)helper.Call(); + _invokeJsTester = (JSObject)Runtime.GetGlobalObject("javaScriptTestHelper"); + } + } + } +} diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.js b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.js new file mode 100644 index 00000000000000..7c83aea5150598 --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/JavaScriptTestHelper.js @@ -0,0 +1,110 @@ +class JSData { + constructor(name) { + this.name = name; + } + toString() { + return `JSData("${this.name}")`; + } +} + +class JSTestError extends Error { + constructor(message) { + super(message) + } +} + + +class JavaScriptTestHelper { + createData(name) { + //console.log(`createData(name:"${name ? name : ''}")`) + return new JSData(name); + } + + createException(name) { + //console.log(`createException(name:"${name ? name : ''}")`) + return new JSTestError(name); + } + + echo1(arg1) { + // console.log(`echo1(arg1:${arg1 !== null ? JSON.stringify(arg1): ''})`) + return arg1; + } + + store1(arg1) { + // console.log(`store1(arg1:${arg1 !== null ? arg1 : ''})`) + globalThis.javaScriptTestHelper.store1val = arg1; + } + + retrieve1() { + const val = globalThis.javaScriptTestHelper.store1val; + // console.log(`retrieve1(arg1:${val !== null ? val : ''})`) + return val; + } + + throw0() { + //console.log(`throw0()`) + throw new Error('throw-0-msg'); + } + + throw1(arg1) { + //console.log(`throw1(arg1:${arg1 !== null ? arg1 : ''})`) + throw new Error('throw1-msg ' + arg1); + } + + throwretrieve1() { + const val = globalThis.javaScriptTestHelper.store1val; + //console.log(`retrieve1(arg1:${val !== null ? val : ''})`) + throw new Error('throwretrieve1 ' + val); + } + + identity1(arg1) { + const val = globalThis.javaScriptTestHelper.store1val; + //console.log(`compare1(arg1:${arg1 !== null ? arg1 : ''}) with ${val !== null ? val : ''}`) + if (val instanceof Date) { + return arg1.valueOf() == val.valueOf(); + } + if (Number.isNaN(val)) { + return Number.isNaN(arg1); + } + return arg1 === val; + } + + getType1() { + const val = globalThis.javaScriptTestHelper.store1val; + const vtype = typeof (val); + // console.log(`getType1(arg1:${vtype !== null ? vtype : ''})`) + return vtype; + } + + getClass1() { + const val = globalThis.javaScriptTestHelper.store1val; + const cname = val.constructor.name; + // console.log(`getClass1(arg1:${cname !== null ? cname : ''})`) + return cname; + } + + invoke1(arg1, name) { + // console.log(`invoke1: ${name}(arg1:${arg1 !== null ? arg1 : ''})`) + const fn = globalThis.JavaScriptTestHelper[name]; + + // console.log("invoke1:" + fn.toString()); + const res = fn(arg1); + // console.log(`invoke1: res ${res !== null ? res : ''})`) + return res; + } + + async awaitvoid(arg1) { + //console.log("awaitvoid:" + typeof arg1); + await arg1; + //console.log("awaitvoid done"); + } + + async await1(arg1) { + console.log("await1:" + typeof arg1); + const value = await arg1; + return value; + } +} + +globalThis.javaScriptTestHelper = new JavaScriptTestHelper(); +// console.log('JavaScriptTestHelper:' Object.keys(globalThis.JavaScriptTestHelper)); diff --git a/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Vector3Marshaler.cs b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Vector3Marshaler.cs new file mode 100644 index 00000000000000..a3644db9908efe --- /dev/null +++ b/src/libraries/System.Private.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Vector3Marshaler.cs @@ -0,0 +1,55 @@ +// 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.CompilerServices; + +namespace System.Runtime.InteropServices.JavaScript.Tests +{ + [StructLayout(LayoutKind.Sequential)] + public struct Vector3 + { + public double X; + public double Y; + public double Z; + } + + public class Vector3Marshaler : JavaScriptMarshalerBase + { + protected override int FixedBufferLength => 3 * sizeof(double); + protected override MarshalToManagedDelegate ToManaged => MarshalToManaged; + protected override MarshalToJavaScriptDelegate ToJavaScript => MarshalToJs; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe Vector3 MarshalToManaged(JavaScriptMarshalerArg arg) + { + var ptr = (Vector3*)arg.ExtraBufferPtr; + return *ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void MarshalToJs(ref Vector3 value, JavaScriptMarshalerArg arg) + { + var ptr = (Vector3*)arg.ExtraBufferPtr; + *ptr = value; + } + protected override string JavaScriptCode => @"function createMarshaller(helpers) { + const { MONO, get_extra_buffer } = helpers; + return { + toManaged: (arg, value) => { + const buf = get_extra_buffer(arg); + MONO.setF64(buf, value.X); + MONO.setF64(buf + 8, value.Y); + MONO.setF64(buf + 16, value.Z); + }, + toJavaScript: (arg) => { + const buf = get_extra_buffer(arg); + const res = { + X: MONO.getF64(buf), + Y: MONO.getF64(buf + 8), + Z: MONO.getF64(buf + 16), + } + return res; + } + }}"; + } +} diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index fd89a822b744ef..856585429a69a7 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -62,6 +62,10 @@ const linked_functions = [ "mono_wasm_web_socket_close_ref", "mono_wasm_web_socket_abort", "mono_wasm_compile_function", + "mono_wasm_bind_js_function", + "mono_wasm_invoke_bound_function", + "mono_wasm_bind_cs_function", + "mono_wasm_register_custom_marshaller", // pal_icushim_static.c "mono_wasm_load_icu_data", diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index 8cc2ff15a1ed4d..7df13c155b5017 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -36,6 +36,12 @@ extern void mono_wasm_web_socket_close_ref (int webSocket_js_handle, int code, M extern void mono_wasm_web_socket_abort (int webSocket_js_handle, int *is_exception, MonoString **result); extern MonoObject* mono_wasm_compile_function (MonoString *str, int *is_exception); +extern MonoString* mono_wasm_bind_js_function(MonoString *function_name, void *signature, int* function_js_handle, int *is_exception); +extern void mono_wasm_invoke_bound_function(int function_js_handle, void *data); +extern MonoString* mono_wasm_bind_cs_function(MonoString *fully_qualified_name, int signature_hash, MonoString *export_as_name, void* signatures, int *is_exception); +extern MonoString* mono_wasm_register_custom_marshaller(MonoString *factory_code, MonoType* type_handle, int* js_handle_out, int *is_exception); + + void core_initialize_internals () { mono_add_internal_call ("Interop/Runtime::InvokeJSWithArgs", mono_wasm_invoke_js_with_args); @@ -57,6 +63,12 @@ void core_initialize_internals () mono_add_internal_call ("Interop/Runtime::WebSocketCloseRef", mono_wasm_web_socket_close_ref); mono_add_internal_call ("Interop/Runtime::WebSocketAbort", mono_wasm_web_socket_abort); mono_add_internal_call ("Interop/Runtime::CancelPromise", mono_wasm_cancel_promise); + + mono_add_internal_call ("System.Runtime.InteropServices.JavaScript.Private.JavaScriptMarshalImpl::_BindJSFunction", mono_wasm_bind_js_function); + mono_add_internal_call ("System.Runtime.InteropServices.JavaScript.Private.JavaScriptMarshalImpl::_InvokeBoundJSFunction", mono_wasm_invoke_bound_function); + mono_add_internal_call ("System.Runtime.InteropServices.JavaScript.Private.JavaScriptMarshalImpl::_BindCSFunction", mono_wasm_bind_cs_function); + mono_add_internal_call ("System.Runtime.InteropServices.JavaScript.Private.JavaScriptMarshalImpl::_RegisterCustomMarshaller", mono_wasm_register_custom_marshaller); + } // Int8Array | int8_t | byte or SByte (signed byte) diff --git a/src/mono/wasm/runtime/corebindings.ts b/src/mono/wasm/runtime/corebindings.ts index 82be21ce2a2ade..51755cbaf512d5 100644 --- a/src/mono/wasm/runtime/corebindings.ts +++ b/src/mono/wasm/runtime/corebindings.ts @@ -1,9 +1,10 @@ // 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, MonoObjectRef } from "./types"; +import { JSHandle, GCHandle, MonoString, MonoObjectRef } from "./types"; import { PromiseControl } from "./cancelable-promise"; import { runtimeHelpers } from "./imports"; +import { JavaScriptMarshalerArguments } from "./marshal"; const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMarshalString*/][] = [ ["_get_cs_owned_object_by_js_handle_ref", "GetCSOwnedObjectByJSHandleRef", "iim"], @@ -27,6 +28,7 @@ const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMar ["_create_date_time_ref", "CreateDateTimeRef", "dm"], ["_create_uri_ref", "CreateUriRef", "sm"], ["_is_simple_array_ref", "IsSimpleArrayRef", "m"], + ["_invoke_bound_cs_function", "InvokeBoundCsFunction", "#!"], ]; export interface t_CSwraps { @@ -53,6 +55,7 @@ export interface t_CSwraps { _create_date_time_ref(ticks: number, result: MonoObjectRef): void; _create_uri_ref(uri: string, result: MonoObjectRef): void; _is_simple_array_ref(obj: MonoObjectRef): boolean; + _invoke_bound_cs_function(bound_function_gc_handle: GCHandle, args: JavaScriptMarshalerArguments): MonoString; } const wrapped_cs_functions: t_CSwraps = {}; diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index 861824f56d77bd..35d4ede08de681 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -3,11 +3,13 @@ import { assert, + GCHandle, MonoArray, MonoAssembly, MonoClass, MonoMethod, MonoObject, MonoString, MonoType, MonoObjectRef, MonoStringRef } from "./types"; import { Module } from "./imports"; +import { JavaScriptMarshalerArguments } from "./marshal"; import { VoidPtr, CharPtrPtr, Int32Ptr, CharPtr, ManagedPointer } from "./types/emscripten"; const fn_signatures: [ident: string, returnType: string | null, argTypes?: string[], opts?: any][] = [ @@ -74,6 +76,8 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_enable_on_demand_gc", "void", ["number"]], ["mono_profiler_init_aot", "void", ["number"]], ["mono_wasm_exec_regression", "number", ["number", "string"]], + ["mono_wasm_invoke_method_dynamic", "number", ["number", "number", "number"]], + ["mono_wasm_invoke_method_bound", "number", ["number", "number"]], ["mono_wasm_write_managed_pointer_unsafe", "void", ["number", "number"]], ["mono_wasm_copy_managed_pointer", "void", ["number", "number"]], ]; @@ -170,6 +174,8 @@ export interface t_Cwraps { mono_wasm_exec_regression(verbose_level: number, image: string): number; mono_wasm_write_managed_pointer_unsafe(destination: VoidPtr | MonoObjectRef, pointer: ManagedPointer): void; mono_wasm_copy_managed_pointer(destination: VoidPtr | MonoObjectRef, source: VoidPtr | MonoObjectRef): void; + mono_wasm_invoke_method_dynamic(method: MonoMethod, bound_function_gc_handle: GCHandle, args: JavaScriptMarshalerArguments): MonoString; + mono_wasm_invoke_method_bound(method: MonoMethod, args: JavaScriptMarshalerArguments): MonoString; } const wrapped_c_functions: t_Cwraps = {}; diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 3e5083662d8601..e89342ddb7d3ff 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -125,12 +125,21 @@ interface WasmRoot { toString(): string; } +declare type GCHandle = { + __brand: "GCHandle"; +}; +declare type JSHandle = { + __brand: "JSHandle"; +}; interface MonoObject extends ManagedPointer { __brandMonoObject: "MonoObject"; } interface MonoString extends MonoObject { __brand: "MonoString"; } +interface MonoType extends ManagedPointer { + __brand: "MonoType"; +} interface MonoArray extends MonoObject { __brand: "MonoArray"; } @@ -237,6 +246,41 @@ declare type DotnetModuleConfigImports = { declare function mono_wasm_runtime_ready(): void; +interface JavaScriptMarshalerArguments extends NativePointer { + __brand: "JavaScriptMarshalerArgs"; +} +interface JavaScriptMarshalerSignature extends NativePointer { + __brand: "JavaScriptMarshalerSignatures"; +} +interface JavaScriptMarshalerArg extends NativePointer { + __brand: "JavaScriptMarshalerArg"; +} +declare function get_arg(args: JavaScriptMarshalerArguments, index: number): JavaScriptMarshalerArg; +declare function get_signature_type(signature: JavaScriptMarshalerSignature, index: number): MonoType; +declare function get_arg_type(arg: JavaScriptMarshalerArg): MonoType; +declare function set_arg_type(arg: JavaScriptMarshalerArg, type: MonoType): void; +declare function get_root_ref(arg: JavaScriptMarshalerArg): MonoObject; +declare function get_extra_buffer(arg: JavaScriptMarshalerArg): NativePointer; +declare function set_root_ref(arg: JavaScriptMarshalerArg, reference: MonoObject): void; +declare function get_arg_b8(arg: JavaScriptMarshalerArg): boolean; +declare function get_arg_u8(arg: JavaScriptMarshalerArg): number; +declare function get_arg_i16(arg: JavaScriptMarshalerArg): number; +declare function get_arg_i32(arg: JavaScriptMarshalerArg): number; +declare function get_arg_i64(arg: JavaScriptMarshalerArg): number; +declare function get_arg_date(arg: JavaScriptMarshalerArg): Date; +declare function get_arg_f64(arg: JavaScriptMarshalerArg): number; +declare function set_arg_b8(arg: JavaScriptMarshalerArg, value: boolean): void; +declare function set_arg_u8(arg: JavaScriptMarshalerArg, value: number): void; +declare function set_arg_i16(arg: JavaScriptMarshalerArg, value: number): void; +declare function set_arg_i32(arg: JavaScriptMarshalerArg, value: number): void; +declare function set_arg_i64(arg: JavaScriptMarshalerArg, value: number): void; +declare function set_arg_date(arg: JavaScriptMarshalerArg, value: Date): void; +declare function set_arg_f64(arg: JavaScriptMarshalerArg, value: number): void; +declare function get_js_handle(arg: JavaScriptMarshalerArg): JSHandle; +declare function set_js_handle(arg: JavaScriptMarshalerArg, jsHandle: JSHandle): void; +declare function get_gc_handle(arg: JavaScriptMarshalerArg): GCHandle; +declare function set_gc_handle(arg: JavaScriptMarshalerArg, gcHandle: GCHandle): void; + declare function mono_wasm_setenv(name: string, value: 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; @@ -403,6 +447,40 @@ interface DotnetPublicAPI { Configuration: string; }; } +declare type MarshalerHelpers = { + mono_type: MonoType; + MONO: typeof MONO; + BINDING: typeof BINDING; + get_arg: typeof get_arg; + get_gc_handle: typeof get_gc_handle; + get_js_handle: typeof get_js_handle; + get_signature_type: typeof get_signature_type; + get_root_ref: typeof get_root_ref; + get_arg_type: typeof get_arg_type; + get_arg_b8: typeof get_arg_b8; + get_arg_u8: typeof get_arg_u8; + get_arg_i16: typeof get_arg_i16; + get_arg_i32: typeof get_arg_i32; + get_arg_f64: typeof get_arg_f64; + get_arg_i64: typeof get_arg_i64; + get_arg_date: typeof get_arg_date; + set_gc_handle: typeof set_gc_handle; + set_js_handle: typeof set_js_handle; + set_root_ref: typeof set_root_ref; + set_arg_type: typeof set_arg_type; + set_arg_b8: typeof set_arg_b8; + set_arg_u8: typeof set_arg_u8; + set_arg_i16: typeof set_arg_i16; + set_arg_i32: typeof set_arg_i32; + set_arg_f64: typeof set_arg_f64; + set_arg_i64: typeof set_arg_i64; + set_arg_date: typeof set_arg_date; + get_extra_buffer: typeof get_extra_buffer; +}; +declare type MarshallerFactory = (helpers: MarshalerHelpers) => { + toManaged: (arg: JavaScriptMarshalerArg, value: any) => void; + toJavaScript: (arg: JavaScriptMarshalerArg) => any; +}; declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: DotnetPublicAPI) => DotnetModuleConfig)): Promise; declare type CreateDotnetRuntimeType = typeof createDotnetRuntime; @@ -410,4 +488,4 @@ declare global { function getDotnetRuntime(runtimeId: number): DotnetPublicAPI | undefined; } -export { BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, MONOType, MonoArray, MonoObject, MonoString, VoidPtr, createDotnetRuntime as default }; +export { BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, MONOType, MarshallerFactory, MonoArray, MonoObject, MonoString, VoidPtr, createDotnetRuntime as default }; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index 929631ec72e3c3..f5900f951d540b 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -698,6 +698,45 @@ mono_wasm_invoke_method (MonoMethod *method, MonoObject *this_arg, void *params[ return result; } +// specialization for calling static void methods with two arguments +EMSCRIPTEN_KEEPALIVE MonoObject* +mono_wasm_invoke_method_dynamic (MonoMethod *method, int bound_function_gc_handle, void* args)// JavaScriptMarshalerArguments +{ + MonoObject *exc = NULL; + MonoObject *res; + + void *invoke_args[2] = {&bound_function_gc_handle, &args }; + + mono_runtime_invoke (method, NULL, invoke_args, &exc); + if (exc) { + MonoObject *exc2 = NULL; + res = (MonoObject*)mono_object_to_string (exc, &exc2); + if (exc2) + res = (MonoObject*) mono_string_new (root_domain, "Exception Double Fault"); + return res; + } + return NULL; +} + +EMSCRIPTEN_KEEPALIVE MonoObject* +mono_wasm_invoke_method_bound (MonoMethod *method, void* args)// JavaScriptMarshalerArguments +{ + MonoObject *exc = NULL; + MonoObject *res; + + void *invoke_args[1] = { args }; + + mono_runtime_invoke (method, NULL, invoke_args, &exc); + if (exc) { + MonoObject *exc2 = NULL; + res = (MonoObject*)mono_object_to_string (exc, &exc2); + if (exc2) + res = (MonoObject*) mono_string_new (root_domain, "Exception Double Fault"); + return res; + } + return NULL; +} + EMSCRIPTEN_KEEPALIVE MonoMethod* mono_wasm_assembly_get_entry_point (MonoAssembly *assembly) { diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 1b8586b0332640..baa16dd0545743 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -99,6 +99,10 @@ const linked_functions = [ "mono_wasm_web_socket_close_ref", "mono_wasm_web_socket_abort", "mono_wasm_compile_function", + "mono_wasm_bind_js_function", + "mono_wasm_invoke_bound_function", + "mono_wasm_bind_cs_function", + "mono_wasm_register_custom_marshaller", // pal_icushim_static.c "mono_wasm_load_icu_data", diff --git a/src/mono/wasm/runtime/export-types.ts b/src/mono/wasm/runtime/export-types.ts index 3cb8c0df090773..442a2957a4b54d 100644 --- a/src/mono/wasm/runtime/export-types.ts +++ b/src/mono/wasm/runtime/export-types.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 { BINDINGType, DotnetPublicAPI, MONOType } from "./exports"; +import { BINDINGType, DotnetPublicAPI, MarshallerFactory, MONOType } from "./exports"; import { DotnetModuleConfig, MonoArray, MonoObject, MonoString } from "./types"; import { EmscriptenModule, VoidPtr } from "./types/emscripten"; @@ -24,6 +24,7 @@ export { VoidPtr, MonoObject, MonoString, MonoArray, BINDINGType, MONOType, EmscriptenModule, - DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType + DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType, + MarshallerFactory }; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 37578887dded68..3cfecd6d3db644 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -29,7 +29,7 @@ import { mono_wasm_stringify_as_error_with_stack, } from "./debug"; import { ENVIRONMENT_IS_WEB, ExitStatusError, runtimeHelpers, setImportsAndExports } from "./imports"; -import { DotnetModuleConfigImports, DotnetModule } from "./types"; +import { DotnetModuleConfigImports, DotnetModule, MonoType } from "./types"; import { mono_load_runtime_and_bcl_args, mono_wasm_load_config, mono_wasm_setenv, mono_wasm_set_runtime_options, @@ -67,6 +67,10 @@ import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; import { mono_run_main, mono_run_main_and_exit } from "./run"; +import { mono_wasm_bind_js_function, mono_wasm_invoke_bound_function } from "./invoke-js"; +import { mono_wasm_bind_cs_function } from "./invoke-cs"; +import { get_arg, get_arg_b8, get_arg_date, get_arg_f64, get_arg_i16, get_arg_i32, get_arg_i64, get_arg_type, get_arg_u8, get_extra_buffer, get_gc_handle, get_js_handle, get_root_ref, get_signature_type, JavaScriptMarshalerArg, mono_wasm_register_custom_marshaller, set_arg_b8, set_arg_date, set_arg_f64, set_arg_i16, set_arg_i32, set_arg_i64, set_arg_type, set_arg_u8, set_gc_handle, set_js_handle, set_root_ref } from "./marshal"; +import { mono_wasm_reject_task, mono_wasm_resolve_task } from "./marshal-to-js"; const MONO = { // current "public" MONO API @@ -113,6 +117,7 @@ const MONO = { }; export type MONOType = typeof MONO; +//current "public" BINDING API const BINDING = { //current "public" BINDING API /** @@ -338,6 +343,10 @@ export const __linker_exports: any = { mono_wasm_web_socket_close_ref, mono_wasm_web_socket_abort, mono_wasm_compile_function, + mono_wasm_bind_js_function, + mono_wasm_invoke_bound_function, + mono_wasm_bind_cs_function, + mono_wasm_register_custom_marshaller, // also keep in sync with pal_icushim_static.c mono_wasm_load_icu_data, @@ -379,6 +388,9 @@ const INTERNAL: any = { mono_wasm_raise_debug_event, mono_wasm_change_debugger_log_level, mono_wasm_runtime_is_ready: runtimeHelpers.mono_wasm_runtime_is_ready, + + mono_wasm_resolve_task, + mono_wasm_reject_task }; @@ -409,4 +421,41 @@ class RuntimeList { const wr = this.list[runtimeId]; return wr ? wr.deref() : undefined; } +} + +// TODO this is new API surface, review +export type MarshalerHelpers = { + mono_type: MonoType, + MONO: typeof MONO, + BINDING: typeof BINDING, + get_arg: typeof get_arg, + get_gc_handle: typeof get_gc_handle, + get_js_handle: typeof get_js_handle, + get_signature_type: typeof get_signature_type, + get_root_ref: typeof get_root_ref, + get_arg_type: typeof get_arg_type, + get_arg_b8: typeof get_arg_b8, + get_arg_u8: typeof get_arg_u8, + get_arg_i16: typeof get_arg_i16, + get_arg_i32: typeof get_arg_i32, + get_arg_f64: typeof get_arg_f64, + get_arg_i64: typeof get_arg_i64, + get_arg_date: typeof get_arg_date, + set_gc_handle: typeof set_gc_handle, + set_js_handle: typeof set_js_handle, + set_root_ref: typeof set_root_ref, + set_arg_type: typeof set_arg_type, + set_arg_b8: typeof set_arg_b8, + set_arg_u8: typeof set_arg_u8, + set_arg_i16: typeof set_arg_i16, + set_arg_i32: typeof set_arg_i32, + set_arg_f64: typeof set_arg_f64, + set_arg_i64: typeof set_arg_i64, + set_arg_date: typeof set_arg_date, + get_extra_buffer: typeof get_extra_buffer, +} + +export type MarshallerFactory = (helpers: MarshalerHelpers) => { + toManaged: (arg: JavaScriptMarshalerArg, value: any) => void, + toJavaScript: (arg: JavaScriptMarshalerArg) => any, } \ No newline at end of file diff --git a/src/mono/wasm/runtime/gc-handles.ts b/src/mono/wasm/runtime/gc-handles.ts index 8ee1cb3fcc2a26..51460693f9e7c4 100644 --- a/src/mono/wasm/runtime/gc-handles.ts +++ b/src/mono/wasm/runtime/gc-handles.ts @@ -14,7 +14,7 @@ const _cs_owned_objects_by_js_handle: any[] = []; const _js_handle_free_list: JSHandle[] = []; let _next_js_handle = 1; -const _js_owned_object_table = new Map(); +export const _js_owned_object_table = new Map(); // NOTE: FinalizationRegistry and WeakRef are missing on Safari below 14.1 if (_use_finalization_registry) { diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts new file mode 100644 index 00000000000000..c7401b9dd10546 --- /dev/null +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -0,0 +1,217 @@ +import { MonoString } from "./export-types"; +import { INTERNAL, Module } from "./imports"; +import { js_to_cs_marshalers } from "./marshal-to-cs"; +import { cs_to_js_marshalers, marshal_exception_to_js } from "./marshal-to-js"; +import { + get_arg, get_sig_buffer_offset, get_sig_type, get_sig_use_root, get_sig, + set_arg_type, set_extra_buffer, set_root, + JavaScriptMarshalerArguments, JavaScriptMarshalerArgSize, JavaScriptMarshalerSignature, get_signature_buffer_length, get_signature_argument_count, is_args_exception, get_custom_marshalers, get_signature_type, +} from "./marshal"; +import { parseFQN, wrap_error } from "./method-calls"; +import { mono_wasm_new_root_buffer, WasmRootBuffer } from "./roots"; +import { conv_string } from "./strings"; +import { assert, MonoStringNull, MonoTypeNull, NativePointerNull } from "./types"; +import { Int32Ptr, NativePointer, VoidPtr } from "./types/emscripten"; +import cwraps, { wrap_c_function } from "./cwraps"; +import { find_method, _get_type_name } from "./method-binding"; + +const exportedMethods = new Map(); + +// TODO replace mono_bind_static_method with this +export function mono_bind_static_method2(fqn: string): Function { + const fn = exportedMethods.get(fqn); + assert(fn, () => `Function ${fqn} has to be marked with [JSExportAttribute]`); + return fn; +} + +const bound_function_symbol = Symbol.for("wasm bound_cs_function"); + +export function mono_wasm_bind_cs_function(fully_qualified_name: MonoString, signature_hash: number, export_as_name: MonoString, signature: JavaScriptMarshalerSignature, is_exception: Int32Ptr): MonoString { + const anyModule = Module as any; + try { + //TODO generate code + + const args_count = get_signature_argument_count(signature); + const extra_buffer_length = get_signature_buffer_length(signature); + const js_fqn = conv_string(fully_qualified_name); + assert(js_fqn, "fully_qualified_name must be string"); + + + const { assembly, namespace, classname, methodname } = parseFQN(js_fqn); + + const asm = cwraps.mono_wasm_assembly_load(assembly); + if (!asm) + throw new Error("Could not find assembly: " + assembly); + + const klass = cwraps.mono_wasm_assembly_find_class(asm, namespace, classname); + if (!klass) + throw new Error("Could not find class: " + namespace + ":" + classname + " in assembly " + assembly); + + const wrapper_name = `__Wrapper_${methodname}_${signature_hash}`; + const method = find_method(klass, wrapper_name, -1); + if (!method) + throw new Error(`Could not find method: ${wrapper_name} in ${klass} [${assembly}]`); + + const { cs_to_js_custom_marshalers, js_to_cs_custom_marshalers } = get_custom_marshalers(signature); + + const closure: any = { + method, get_arg, signature, + stackSave: anyModule.stackSave, stackAlloc: anyModule.stackAlloc, stackRestore: anyModule.stackRestore, + mono_wasm_new_root_buffer, init_void, init_result, init_argument, marshal_exception_to_js, is_args_exception, + mono_wasm_invoke_method_bound: wrap_c_function("mono_wasm_invoke_method_bound"), + }; + const bound_js_function_name = "_bound_" + `${namespace}_${classname}_${methodname}`.replaceAll(".", "_"); + let body = `//# sourceURL=https://mono-wasm.invalid/${bound_js_function_name} \n`; + let converter_names = ""; + for (let index = 0; index < args_count; index++) { + converter_names += `, converter${index + 1}`; + } + const res_mono_type = get_signature_type(signature, 1); + if (res_mono_type !== MonoTypeNull) { + converter_names += ", resultconverter"; + } + body += `const { method, get_arg, signature, stackSave, stackAlloc, stackRestore, mono_wasm_new_root_buffer, init_void, init_result, init_argument, marshal_exception_to_js, is_args_exception, mono_wasm_invoke_method_bound ${converter_names} } = closure;\n`; + body += `return function ${bound_js_function_name} () {\n`; + body += "let roots = null;\n"; + body += "const sp = stackSave();\n"; + body += "try {\n"; + body += ` roots = mono_wasm_new_root_buffer(${args_count + 2}); // TODO, optimize to pool allocation\n`; + body += ` const args = stackAlloc(${(args_count + 2) * JavaScriptMarshalerArgSize + extra_buffer_length});\n`; + body += ` const extra = args + ${(args_count + 2) * JavaScriptMarshalerArgSize};\n`; + if (res_mono_type !== MonoTypeNull) { + body += " init_result(roots, args, extra, signature);\n"; + } else { + body += " init_void(roots, args, extra, signature);\n"; + } + for (let index = 0; index < args_count; index++) { + body += ` init_argument(${1 + index}, roots, args, extra, signature);\n`; + } + for (let index = 0; index < args_count; index++) { + const arg_offset = (index + 2) * JavaScriptMarshalerArgSize; + const mono_type = get_signature_type(signature, index + 2); + let converter = js_to_cs_marshalers.get(mono_type); + if (!converter) { + converter = js_to_cs_custom_marshalers.get(mono_type); + } + assert(converter && typeof converter === "function", () => `Unknow converter for type ${_get_type_name(mono_type)} at ${index} `); + const converter_name = `converter${index + 1}`; + closure[converter_name] = converter; + + body += ` ${converter_name}(args + ${arg_offset}, arguments[${index}]); // ${_get_type_name(mono_type)} \n`; + } + body += " const fail = mono_wasm_invoke_method_bound(method, args);\n"; + body += " if (fail) throw new Error(\"ERR22: Unexpected error: \" + conv_string(fail));\n"; + body += " if (is_args_exception(args)) throw marshal_exception_to_js(get_arg(args, 0));\n"; + + if (res_mono_type === MonoTypeNull) { + // TODO emit assert + } else { + let converter = cs_to_js_marshalers.get(res_mono_type); + if (!converter) { + converter = cs_to_js_custom_marshalers.get(res_mono_type); + } + assert(converter && typeof converter === "function", () => `Unknow converter for type ${res_mono_type} at ret`); + closure["resultconverter"] = converter; + + body += ` return resultconverter(args + ${JavaScriptMarshalerArgSize}); // ${_get_type_name(res_mono_type)} \n`; + } + + body += "} finally {\n"; + body += " stackRestore(sp);\n"; + body += " if(roots) roots.release()\n"; + body += "}}"; + //console.log("-------"); + //console.log(body); + //console.log("-------"); + const factory = new Function("closure", body); + const bound_fn = factory(closure); + bound_fn[bound_function_symbol] = true; + + exportedMethods.set(js_fqn, bound_fn); + + if (export_as_name) { + const js_export_name = conv_string(export_as_name); + assert(js_export_name, "export_as_name must be string"); + _walk_global_scope_to_set_function(js_export_name, bound_fn); + } + + return MonoStringNull; + } + catch (ex) { + return wrap_error(is_exception, ex); + } +} + +function init_void(roots: WasmRootBuffer, args: JavaScriptMarshalerArguments) { + assert(args && (args) % 8 == 0, "Arg alignment"); + const exc = get_arg(args, 0); + const excRoot = roots.get_address(0); + set_arg_type(exc, MonoTypeNull); + set_root(exc, excRoot); + + const res = get_arg(args, 1); + set_arg_type(res, MonoTypeNull); + set_root(res, NativePointerNull); + set_extra_buffer(res, NativePointerNull); +} + +function init_result(roots: WasmRootBuffer, args: JavaScriptMarshalerArguments, extra: NativePointer, signature: JavaScriptMarshalerSignature) { + assert(args && (args) % 8 == 0, "Arg alignment"); + const exc = get_arg(args, 0); + const excRoot = roots.get_address(0); + set_arg_type(exc, MonoTypeNull); + set_root(exc, excRoot); + + const res = get_arg(args, 1); + const resSig = get_sig(signature, 1); + + set_arg_type(res, get_sig_type(resSig)); + const bufferOffset = get_sig_buffer_offset(resSig); + const useRoot = get_sig_use_root(resSig); + if (bufferOffset != -1) { + set_extra_buffer(res, extra + bufferOffset); + } + if (useRoot != 0) { + set_root(res, roots.get_address(1)); + } +} +// arg1 is at position=1 +function init_argument(position: number, roots: WasmRootBuffer, args: JavaScriptMarshalerArguments, extra: VoidPtr, signature: JavaScriptMarshalerSignature) { + assert(args && (args) % 8 == 0, "Arg alignment"); + + const arg = get_arg(args, position + 1); + const sig = get_sig(signature, position + 1); + + set_arg_type(arg, get_sig_type(sig)); + const bufferOffset = get_sig_buffer_offset(sig); + const useRoot = get_sig_use_root(sig); + if (bufferOffset != -1) { + set_extra_buffer(arg, extra + bufferOffset); + } + if (useRoot != 0) { + set_root(arg, roots.get_address(position + 1)); + } +} + +function _walk_global_scope_to_set_function(str: string, fn: Function): void { + let scope: any = globalThis; + const parts = str.split("."); + if (parts[0] === "INTERNAL") { + scope = INTERNAL; + parts.shift(); + } + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + let newscope = scope[part]; + if (!newscope) { + newscope = {}; + scope[part] = newscope; + } + assert(newscope, () => `${part} not found while looking up ${str}`); + scope = newscope; + } + + const fname = parts[parts.length - 1]; + scope[fname] = fn; +} diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts new file mode 100644 index 00000000000000..c3128299a29e68 --- /dev/null +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -0,0 +1,119 @@ +import { mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle } from "./gc-handles"; +import { js_to_cs_marshalers, marshal_exception_to_cs } from "./marshal-to-cs"; +import { cs_to_js_marshalers } from "./marshal-to-js"; +import { get_signature_argument_count, get_signature_type, get_custom_marshalers, JavaScriptMarshalerArguments as JavaScriptMarshalerArguments, JavaScriptMarshalerArgSize, JavaScriptMarshalerSignature as JavaScriptMarshalerSignature } from "./marshal"; +import { setI32 } from "./memory"; +import { _get_type_name } from "./method-binding"; +import { wrap_error } from "./method-calls"; +import { conv_string } from "./strings"; +import { assert, JSHandle, MonoString, MonoStringNull, MonoTypeNull } from "./types"; +import { Int32Ptr } from "./types/emscripten"; +import { INTERNAL } from "./imports"; + +const bound_function_symbol = Symbol.for("wasm bound_js_function"); + +export function mono_wasm_bind_js_function(function_name: MonoString, signature: JavaScriptMarshalerSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr): MonoString { + try { + const fn = mono_wasm_lookup_function(function_name); + const args_count = get_signature_argument_count(signature); + + const { cs_to_js_custom_marshalers, js_to_cs_custom_marshalers } = get_custom_marshalers(signature); + + const closure: any = { fn, assert, marshal_exception_to_cs }; + const js_function_name = conv_string(function_name)!; + const bound_js_function_name = "_bound_" + js_function_name.replaceAll(".", "_"); + let body = `//# sourceURL=https://mono-wasm.invalid/${bound_js_function_name} \n`; + let converter_names = ""; + for (let index = 0; index < args_count; index++) { + converter_names += `, converter${index + 1}`; + } + const res_mono_type = get_signature_type(signature, 1); + if (res_mono_type !== MonoTypeNull) { + converter_names += ", resultconverter"; + } + + body += `const { fn, assert, marshal_exception_to_cs ${converter_names} } = closure;\n`; + body += `return function ${bound_js_function_name} (buffer) { try {\n`; + + let pass_args = ""; + for (let index = 0; index < args_count; index++) { + const arg_offset = (index + 2) * JavaScriptMarshalerArgSize; + const mono_type = get_signature_type(signature, index + 2); + let converter = cs_to_js_marshalers.get(mono_type); + if (!converter) { + converter = cs_to_js_custom_marshalers.get(mono_type); + } + assert(converter, () => `Unknow converter for type ${_get_type_name(mono_type)} at ${index}`); + const converter_name = `converter${index + 1}`; + closure[converter_name] = converter; + + body += ` const arg${index + 1}_js = ${converter_name}(buffer + ${arg_offset}); // ${_get_type_name(mono_type)} \n`; + + if (pass_args === "") { + pass_args += `arg${index + 1}_js`; + } else { + pass_args += `, arg${index + 1}_js`; + } + } + body += ` const res_js = fn(${pass_args});\n`; + + if (res_mono_type === MonoTypeNull) { + body += ` if (res_js !== undefined) throw new Error('Function ${js_function_name} returned unexpected value, C# signature is void');\n`; + } else { + let converter = js_to_cs_marshalers.get(res_mono_type); + if (!converter) { + converter = js_to_cs_custom_marshalers.get(res_mono_type); + } + assert(converter, () => `Unknow converter for type ${res_mono_type} at ret`); + closure["resultconverter"] = converter; + + body += ` resultconverter(buffer + ${JavaScriptMarshalerArgSize}, res_js); // ${_get_type_name(res_mono_type)} \n`; + } + + body += "} catch (ex) {\n"; + body += " marshal_exception_to_cs(buffer, ex);\n"; + body += "}}"; + //console.log("-------"); + //console.log(body); + //console.log("-------"); + const factory = new Function("closure", body); + const bound_fn = factory(closure); + bound_fn[bound_function_symbol] = true; + const bound_function_handle = mono_wasm_get_js_handle(bound_fn)!; + setI32(function_js_handle, bound_function_handle); + return MonoStringNull; + } catch (ex) { + return wrap_error(is_exception, ex); + } +} + +export function mono_wasm_invoke_bound_function(bound_function_js_handle: JSHandle, args: JavaScriptMarshalerArguments): void { + const bound_fn = mono_wasm_get_jsobj_from_js_handle(bound_function_js_handle); + assert(bound_fn && typeof (bound_fn) === "function" && bound_fn[bound_function_symbol], () => `Bound function handle expected ${bound_function_js_handle}`); + bound_fn(args); +} + +function mono_wasm_lookup_function(function_id: MonoString): Function { + const js_function_name = conv_string(function_id); + assert(js_function_name, () => "js_function_name must be string"); + + let scope: any = globalThis; + const parts = js_function_name.split("."); + if (parts[0] === "INTERNAL") { + scope = INTERNAL; + parts.shift(); + } + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const newscope = scope[part]; + assert(newscope, () => `${part} not found while looking up ${js_function_name}`); + scope = newscope; + } + + const fname = parts[parts.length - 1]; + const fn = scope[fname]; + + assert(typeof (fn) === "function", () => `${js_function_name} must be a Function but was ${typeof fn}`); + return fn; +} diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts new file mode 100644 index 00000000000000..0da415ad9ea810 --- /dev/null +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -0,0 +1,199 @@ +import { cs_owned_js_handle_symbol, js_owned_gc_handle_symbol, mono_wasm_get_js_handle, mono_wasm_release_cs_owned_object } from "./gc-handles"; +import { INTERNAL } from "./imports"; +import { + JavaScriptMarshalerArg, ManagedError, + set_gc_handle, set_js_handle, set_root_ref, set_arg_type, set_arg_i32, set_arg_f64, set_arg_i64, set_arg_f32, set_arg_i16, set_arg_u8, set_arg_b8, set_arg_date +} from "./marshal"; +import { knownTypes } from "./startup"; +import { js_string_to_mono_string } from "./strings"; +import { MonoType, MonoTypeNull, assert } from "./types"; + +export const js_to_cs_marshalers = new Map(); + +export type MarshalerToCs = (arg: JavaScriptMarshalerArg, value: any) => void; + +function _marshal_bool_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.bool); + set_arg_b8(arg, value); +} + +function _marshal_byte_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.byte); + set_arg_u8(arg, value); +} + +function _marshal_int16_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.int16); + set_arg_i16(arg, value); +} + +function _marshal_int32_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.int32); + set_arg_i32(arg, value); +} + +function _marshal_int64_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.int64); + set_arg_i64(arg, value); +} + +function _marshal_double_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.double); + set_arg_f64(arg, value); +} + +function _marshal_float_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.float); + set_arg_f32(arg, value); +} + +function _marshal_intptr_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + set_arg_type(arg, knownTypes.intptr); + set_arg_i32(arg, value); +} + +function _marshal_date_time_to_cs(arg: JavaScriptMarshalerArg, value: Date): void { + set_arg_type(arg, knownTypes.date_time); + set_arg_date(arg, value); +} + +function _marshal_date_time_offset_to_cs(arg: JavaScriptMarshalerArg, value: Date): void { + set_arg_type(arg, knownTypes.date_time_offset); + set_arg_date(arg, value); +} + +function _marshal_string_to_cs(arg: JavaScriptMarshalerArg, value: string) { + if (!value) { + set_arg_type(arg, MonoTypeNull); + } + else { + const pStr = js_string_to_mono_string(value)!; + set_root_ref(arg, pStr); + set_arg_type(arg, knownTypes.string); + } +} + +function _marshal_task_to_cs(arg: JavaScriptMarshalerArg, value: Promise) { + if (!value) { + set_arg_type(arg, MonoTypeNull); + return; + } + /*const gc_handle = ((value)[js_owned_gc_handle_symbol]); + if (gc_handle) { + // this is Task round trip + set_arg_type(arg, knownTypes.task); + set_gc_handle(arg, gc_handle); + return; + }*/ + const js_handle = mono_wasm_get_js_handle(value)!; + set_js_handle(arg, js_handle); + set_arg_type(arg, knownTypes.ijs_object); + value.then(data => { + INTERNAL.mono_wasm_resolve_tcs(js_handle, data); + mono_wasm_release_cs_owned_object(js_handle); + }).catch(reason => { + INTERNAL.mono_wasm_reject_tcs(js_handle, reason); + mono_wasm_release_cs_owned_object(js_handle); + }); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function marshal_exception_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + if (!value) { + set_arg_type(arg, MonoTypeNull); + } + else if (value instanceof ManagedError) { + set_arg_type(arg, knownTypes.exception); + // this is managed exception round-trip + const gc_handle = (value)[js_owned_gc_handle_symbol]; + set_gc_handle(arg, gc_handle); + } + else { + set_arg_type(arg, knownTypes.jsexception); + const message = value.toString(); + const pMessage = js_string_to_mono_string(message); + set_root_ref(arg, pMessage); + + const known_js_handle = value[cs_owned_js_handle_symbol]; + if (known_js_handle) { + set_js_handle(arg, known_js_handle); + } + else { + const js_handle = mono_wasm_get_js_handle(value)!; + set_js_handle(arg, js_handle); + } + } +} + +function _marshal_js_object_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + if (value === undefined || value === null) { + set_arg_type(arg, MonoTypeNull); + } + else { + // if value was ManagedObject, it would be double proxied, but the C# signature requires that + assert(!value[js_owned_gc_handle_symbol], "JSObject proxy of ManagedObject proxy is not supported"); + + const js_handle = mono_wasm_get_js_handle(value)!; + set_js_handle(arg, js_handle); + } +} + +function _marshal_cs_object_to_cs(arg: JavaScriptMarshalerArg, value: any): void { + if (value === undefined || value === null) { + set_arg_type(arg, MonoTypeNull); + } + else { + const gc_handle = value[js_owned_gc_handle_symbol]; + if (!gc_handle) { + const js_type = typeof (value); + if (js_type === "string") { + set_arg_type(arg, knownTypes.string); + const pStr = js_string_to_mono_string(value); + set_root_ref(arg, pStr); + } + else if (js_type === "number") { + set_arg_type(arg, knownTypes.double); + set_arg_f64(arg, value); + } + else if (js_type === "boolean") { + set_arg_type(arg, knownTypes.bool); + set_arg_b8(arg, value); + } + else if (value instanceof Date) { + set_arg_type(arg, knownTypes.date_time); + set_arg_date(arg, value); + } + else { + assert(js_type == "object", () => `JSObject proxy is not supported for ${js_type} ${value}`); + const js_handle = mono_wasm_get_js_handle(value); + set_arg_type(arg, knownTypes.ijs_object); + set_js_handle(arg, js_handle); + } + } + else { + set_arg_type(arg, knownTypes.cs_object); + set_gc_handle(arg, gc_handle); + } + } +} + +export function initialize_marshalers_to_cs(): void { + if (js_to_cs_marshalers.size == 0) { + //console.log(JSON.stringify(knownTypes, null, 2)); + js_to_cs_marshalers.set(knownTypes.bool, _marshal_bool_to_cs); + js_to_cs_marshalers.set(knownTypes.byte, _marshal_byte_to_cs); + js_to_cs_marshalers.set(knownTypes.int16, _marshal_int16_to_cs); + js_to_cs_marshalers.set(knownTypes.int32, _marshal_int32_to_cs); + js_to_cs_marshalers.set(knownTypes.int64, _marshal_int64_to_cs); + js_to_cs_marshalers.set(knownTypes.double, _marshal_double_to_cs); + js_to_cs_marshalers.set(knownTypes.float, _marshal_float_to_cs); + js_to_cs_marshalers.set(knownTypes.intptr, _marshal_intptr_to_cs); + js_to_cs_marshalers.set(knownTypes.date_time, _marshal_date_time_to_cs); + js_to_cs_marshalers.set(knownTypes.date_time_offset, _marshal_date_time_offset_to_cs); + js_to_cs_marshalers.set(knownTypes.string, _marshal_string_to_cs); + js_to_cs_marshalers.set(knownTypes.exception, marshal_exception_to_cs); + js_to_cs_marshalers.set(knownTypes.ijs_object, _marshal_js_object_to_cs); + js_to_cs_marshalers.set(knownTypes.cs_object, _marshal_cs_object_to_cs); + js_to_cs_marshalers.set(knownTypes.task, _marshal_task_to_cs); + } +} diff --git a/src/mono/wasm/runtime/marshal-to-js.ts b/src/mono/wasm/runtime/marshal-to-js.ts new file mode 100644 index 00000000000000..1dd26f91afd7be --- /dev/null +++ b/src/mono/wasm/runtime/marshal-to-js.ts @@ -0,0 +1,216 @@ +import { PromiseControl, promise_control_symbol, _create_cancelable_promise } from "./cancelable-promise"; +import { MonoString } from "./export-types"; +import { _lookup_js_owned_object, js_owned_gc_handle_symbol, _use_finalization_registry, _js_owned_object_registry, _register_js_owned_object, mono_wasm_get_jsobj_from_js_handle, _js_owned_object_table } from "./gc-handles"; +import { ManagedObject, get_gc_handle, get_js_handle, get_root_ref, JavaScriptMarshalerArg, ManagedError, get_arg_type, get_arg_i32, get_arg_f64, get_arg_i64, get_arg_i16, get_arg_u8, get_arg_f32, get_arg_b8, get_arg_date } from "./marshal"; +import { _get_type_name } from "./method-binding"; +import { knownTypes } from "./startup"; +import { conv_string } from "./strings"; +import { assert, GCHandle, MonoType, MonoTypeNull } from "./types"; + +export const cs_to_js_marshalers = new Map(); +export type MarshalerToJs = (arg: JavaScriptMarshalerArg) => any; + +function _marshal_bool_to_js(arg: JavaScriptMarshalerArg): boolean { + return get_arg_b8(arg); +} + +function _marshal_byte_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_u8(arg); +} + +function _marshal_int16_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_i16(arg); +} + +function _marshal_int32_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_i32(arg); +} + +function _marshal_int64_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_i64(arg); +} + +function _marshal_float_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_f32(arg); +} + +function _marshal_double_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_f64(arg); +} + +function _marshal_intptr_to_js(arg: JavaScriptMarshalerArg): number { + return get_arg_i32(arg); +} + +function _marshal_datetime_to_js(arg: JavaScriptMarshalerArg): Date { + return get_arg_date(arg); +} + +function _marshal_task_to_js(arg: JavaScriptMarshalerArg): Promise | null { + const type = get_arg_type(arg); + if (type == MonoTypeNull) { + return null; + } + + /* + if (type == knownTypes.ijs_object) { + // this is Proxy roundtrip + const js_handle = get_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; + }*/ + + assert(type == knownTypes.task, "Expecting GCHandle of Task"); + + const gc_handle = get_gc_handle(arg); + + const cleanup = () => _js_owned_object_table.delete(gc_handle); + + const { promise } = _create_cancelable_promise(cleanup, cleanup); + + (promise)[js_owned_gc_handle_symbol] = gc_handle; + + _register_js_owned_object(gc_handle, promise); + + return promise; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function mono_wasm_resolve_task(gc_handle: GCHandle, data: any): void { + const promise = _lookup_js_owned_object(gc_handle); + if (!promise) return; + const promise_control = promise[promise_control_symbol]; + try { + promise_control.resolve(data); + } catch (ex) { + promise_control.reject("" + ex); + } +} + +export function mono_wasm_reject_task(gc_handle: GCHandle, reason: Error): void { + const promise = _lookup_js_owned_object(gc_handle); + if (!promise) return; + const promise_control = promise[promise_control_symbol]; + try { + promise_control.reject(reason); + } catch (ex) { + promise_control.reject("" + ex); + } +} + + +function _marshal_string_to_js(arg: JavaScriptMarshalerArg): string | null { + const type = get_arg_type(arg); + if (type == MonoTypeNull) { + return null; + } + const ref = get_root_ref(arg); + const value = conv_string(ref); + return value; +} + +export function marshal_exception_to_js(arg: JavaScriptMarshalerArg): Error | null { + const type = get_arg_type(arg); + if (type == MonoTypeNull) { + return null; + } + if (type == knownTypes.jsexception) { + // this is JSException roundtrip + const js_handle = get_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; + } + + const gc_handle = get_gc_handle(arg); + let result = _lookup_js_owned_object(gc_handle); + if (!result) { + // this will create new ManagedError + const messagePtr = get_root_ref(arg); + assert(messagePtr, "Null messagePtr"); + const message = conv_string(messagePtr)!; + result = new ManagedError(message); + setup_managed_proxy(result, gc_handle); + } + + return result; +} + +function _marshal_js_object_to_js(arg: JavaScriptMarshalerArg): any { + const type = get_arg_type(arg); + if (type == MonoTypeNull) { + return null; + } + const js_handle = get_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; +} + +function _marshal_cs_object_to_js(arg: JavaScriptMarshalerArg): any { + const mono_type = get_arg_type(arg); + if (mono_type == MonoTypeNull) { + return null; + } + if (mono_type == knownTypes.ijs_object) { + const js_handle = get_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; + } + + if (mono_type == knownTypes.cs_object) { + const gc_handle = get_gc_handle(arg); + if (!gc_handle) { + return null; + } + + // see if we have js owned instance for this gc_handle already + let result = _lookup_js_owned_object(gc_handle); + + // If the JS object for this gc_handle was already collected (or was never created) + if (!result) { + result = new ManagedObject(); + setup_managed_proxy(result, gc_handle); + } + + return result; + } + + // other types + const converter = cs_to_js_marshalers.get(mono_type); + assert(converter, () => `Unknow converter for type ${_get_type_name(mono_type)}`); + return converter(arg); +} + +function setup_managed_proxy(result: any, gc_handle: GCHandle) { + // keep the gc_handle so that we could easily convert it back to original C# object for roundtrip + result[js_owned_gc_handle_symbol] = gc_handle; + + // NOTE: this would be leaking C# objects when the browser doesn't support FinalizationRegistry/WeakRef + if (_use_finalization_registry) { + // register for GC of the C# object after the JS side is done with the object + _js_owned_object_registry.register(result, gc_handle); + } + + // register for instance reuse + // NOTE: this would be leaking C# objects when the browser doesn't support FinalizationRegistry/WeakRef + _register_js_owned_object(gc_handle, result); +} + +export function initialize_marshalers_to_js(): void { + if (cs_to_js_marshalers.size == 0) { + cs_to_js_marshalers.set(knownTypes.bool, _marshal_bool_to_js); + cs_to_js_marshalers.set(knownTypes.byte, _marshal_byte_to_js); + cs_to_js_marshalers.set(knownTypes.int16, _marshal_int16_to_js); + cs_to_js_marshalers.set(knownTypes.int32, _marshal_int32_to_js); + cs_to_js_marshalers.set(knownTypes.int64, _marshal_int64_to_js); + cs_to_js_marshalers.set(knownTypes.float, _marshal_float_to_js); + cs_to_js_marshalers.set(knownTypes.intptr, _marshal_intptr_to_js); + cs_to_js_marshalers.set(knownTypes.double, _marshal_double_to_js); + cs_to_js_marshalers.set(knownTypes.string, _marshal_string_to_js); + cs_to_js_marshalers.set(knownTypes.exception, marshal_exception_to_js); + cs_to_js_marshalers.set(knownTypes.ijs_object, _marshal_js_object_to_js); + cs_to_js_marshalers.set(knownTypes.cs_object, _marshal_cs_object_to_js); + cs_to_js_marshalers.set(knownTypes.date_time, _marshal_datetime_to_js); + cs_to_js_marshalers.set(knownTypes.date_time_offset, _marshal_datetime_to_js); + cs_to_js_marshalers.set(knownTypes.task, _marshal_task_to_js); + } +} diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts new file mode 100644 index 00000000000000..86463f50f360da --- /dev/null +++ b/src/mono/wasm/runtime/marshal.ts @@ -0,0 +1,354 @@ +import { MonoObject, MonoString } from "./export-types"; +import { MarshalerHelpers } from "./exports"; +import { get_js_obj, js_owned_gc_handle_symbol, mono_wasm_get_js_handle } from "./gc-handles"; +import { MONO, BINDING, Module } from "./imports"; +import { MarshalerToCs } from "./marshal-to-cs"; +import { MarshalerToJs } from "./marshal-to-js"; +import { getF32, getF64, getI16, getI32, getI64, getU32, getU8, setF32, setF64, setI16, setI32, setI64, setU32, setU8 } from "./memory"; +import { _get_type_name } from "./method-binding"; +import { wrap_error } from "./method-calls"; +import { conv_string } from "./strings"; +import { assert, GCHandle, JSHandle, JSHandleNull, MonoStringNull, MonoType, MonoTypeNull } from "./types"; +import { Int32Ptr, NativePointer } from "./types/emscripten"; + +/** + * Layout of the marshaling buffers is + * + * signature: pointer to [ + * ArgumentCount: number, + * ExtraBufferLength: number, + * Exception: { type:MonoType, extraOffset:int, extraLength:int, useRoot:bool, marshaler:JSHandle } + * TResult: { type:MonoType, extraOffset:int, extraLength:int, useRoot:bool, marshaler:JSHandle } + * T1: { type:MonoType, extraOffset:int, extraLength:int, useRoot:bool, marshaler:JSHandle } + * T2: { type:MonoType, extraOffset:int, extraLength:int, useRoot:bool, marshaler:JSHandle } + * ... + * ] + * - TRes === MonoTypeNull means void function + * + * data: pointer to [ + * exc: {type:MonoType, handle: IntPtr, data: Int64|Ref*|Void* }, + * res: {type:MonoType, handle: IntPtr, data: Int64|Ref*|Void* }, + * arg1: {type:MonoType, handle: IntPtr, data: Int64|Ref*|Void* }, + * arg2: {type:MonoType, handle: IntPtr, data: Int64|Ref*|Void* }, + * ... + * ] + * for data see JavaScriptMarshalerArg in C#, discriminated union, 16 bytes + */ + + +export const JavaScriptMarshalerArgSize = 16; +export const JavaScriptMarshalerSigSize = 20; +export const JavaScriptMarshalerSigOffset = 8; + +export interface JavaScriptMarshalerArguments extends NativePointer { + __brand: "JavaScriptMarshalerArgs" +} + +export interface JavaScriptMarshalerSignature extends NativePointer { + __brand: "JavaScriptMarshalerSignatures" +} + +export interface JavaScriptMarshalerSig extends NativePointer { + __brand: "JavaScriptMarshalerSig" +} + +export interface JavaScriptMarshalerArg extends NativePointer { + __brand: "JavaScriptMarshalerArg" +} + +export function get_arg(args: JavaScriptMarshalerArguments, index: number): JavaScriptMarshalerArg { + assert(args, "Null args"); + return args + (index * JavaScriptMarshalerArgSize); +} + +export function is_args_exception(args: JavaScriptMarshalerArguments): boolean { + assert(args, "Null args"); + const exceptionType = get_arg_type(args); + return exceptionType !== MonoTypeNull; +} + +export function get_sig(signature: JavaScriptMarshalerSignature, index: number): JavaScriptMarshalerSig { + assert(signature, "Null signatures"); + return signature + (index * JavaScriptMarshalerSigSize) + JavaScriptMarshalerSigOffset; +} + +export function get_signature_type(signature: JavaScriptMarshalerSignature, index: number): MonoType { + assert(signature, "Null signatures"); + const sig = get_sig(signature, index); + return getU32(sig); +} + +export function get_signature_marshaler(signature: JavaScriptMarshalerSignature, index: number): JSHandle { + assert(signature, "Null signatures"); + const sig = get_sig(signature, index); + return getU32(sig + 16); +} + +export function get_signature_argument_count(signature: JavaScriptMarshalerSignature): number { + assert(signature, "Null signatures"); + return getU32(signature); +} + +export function get_signature_buffer_length(signature: JavaScriptMarshalerSignature): number { + assert(signature, "Null signatures"); + return getU32(signature + 4); +} + +export function get_sig_type(sig: JavaScriptMarshalerSig): MonoType { + assert(sig, "Null signatures"); + return getU32(sig); +} + +export function get_sig_buffer_offset(sig: JavaScriptMarshalerSig): number { + assert(sig, "Null signatures"); + return getU32(sig + 4); +} + +export function get_sig_buffer_length(sig: JavaScriptMarshalerSig): number { + assert(sig, "Null signatures"); + return getU32(sig + 8); +} + +export function get_sig_use_root(sig: JavaScriptMarshalerSig): number { + assert(sig, "Null signatures"); + return getU32(sig + 12); +} + +export function get_arg_type(arg: JavaScriptMarshalerArg): MonoType { + assert(arg, "Null arg"); + const type = getU32(arg + 12); + return type; +} + +export function set_arg_type(arg: JavaScriptMarshalerArg, type: MonoType): void { + assert(arg, "Null arg"); + setU32(arg + 12, type); +} + +export function get_root_ref(arg: JavaScriptMarshalerArg): MonoObject { + assert(arg, "Null arg"); + const root = getU32(arg + 8); + assert(root, "Null root"); + return getU32(root); +} + +export function set_root(arg: JavaScriptMarshalerArg, root: NativePointer): void { + assert(arg, "Null arg"); + setU32(arg + 8, root); +} + +export function set_extra_buffer(arg: JavaScriptMarshalerArg, ptr: NativePointer): void { + assert(arg, "Null arg"); + setU32(arg, ptr); +} + +export function get_extra_buffer(arg: JavaScriptMarshalerArg): NativePointer { + assert(arg, "Null arg"); + return getU32(arg); +} + +export function set_root_ref(arg: JavaScriptMarshalerArg, reference: MonoObject): void { + assert(arg, "Null arg"); + const root = getU32(arg + 8); + assert(root, "Null root"); + setU32(root, reference); +} + +export function get_arg_b8(arg: JavaScriptMarshalerArg): boolean { + assert(arg, "Null arg"); + return !!getU8(arg); +} + +export function get_arg_u8(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getU8(arg); +} + +export function get_arg_i16(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getI16(arg); +} + +export function get_arg_i32(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getI32(arg); +} + +export function get_arg_i64(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getI64(arg); +} + +export function get_arg_date(arg: JavaScriptMarshalerArg): Date { + assert(arg, "Null arg"); + const unixTime = getI64(arg); + const date = new Date(unixTime); + return date; +} + +export function get_arg_f32(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getF32(arg); +} + +export function get_arg_f64(arg: JavaScriptMarshalerArg): number { + assert(arg, "Null arg"); + return getF64(arg); +} + +export function set_arg_b8(arg: JavaScriptMarshalerArg, value: boolean): void { + assert(arg, "Null arg"); + setU8(arg, value ? 1 : 0); +} + +export function set_arg_u8(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setU8(arg, value); +} + +export function set_arg_i16(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setI16(arg, value); +} + +export function set_arg_i32(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setI32(arg, value); +} + +export function set_arg_i64(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setI64(arg, value); +} + +export function set_arg_date(arg: JavaScriptMarshalerArg, value: Date): void { + assert(arg, "Null arg"); + // getTime() is always UTC + const unixTime = value.getTime(); + setI64(arg, unixTime); +} + +export function set_arg_f64(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setF64(arg, value); +} + +export function set_arg_f32(arg: JavaScriptMarshalerArg, value: number): void { + assert(arg, "Null arg"); + setF32(arg, value); +} + +export function get_js_handle(arg: JavaScriptMarshalerArg): JSHandle { + assert(arg, "Null arg"); + return getU32(arg + 4); +} + +export function set_js_handle(arg: JavaScriptMarshalerArg, jsHandle: JSHandle): void { + assert(arg, "Null arg"); + setU32(arg + 4, jsHandle); +} + +export function get_gc_handle(arg: JavaScriptMarshalerArg): GCHandle { + assert(arg, "Null arg"); + return getU32(arg + 4); +} + +export function set_gc_handle(arg: JavaScriptMarshalerArg, gcHandle: GCHandle): void { + assert(arg, "Null arg"); + setU32(arg + 4, gcHandle); +} + +export class ManagedObject { + toString(): string { + return `CsObject(gc_handle: ${(this)[js_owned_gc_handle_symbol]})`; + } +} + +export class ManagedError extends Error { + constructor(message: string) { + super(message); + } + + get stack(): string | undefined { + //todo implement lazy + return super.stack; + } + + toString(): string { + return `ManagedError(gc_handle: ${(this)[js_owned_gc_handle_symbol]})`; + } +} + +export type KnownTypes = { + bool: MonoType, + byte: MonoType, + int16: MonoType, + int32: MonoType, + int64: MonoType, + double: MonoType, + float: MonoType, + task: MonoType, + intptr: MonoType, + date_time: MonoType, + date_time_offset: MonoType, + string: MonoType, + ijs_object: MonoType, + cs_object: MonoType + exception: MonoType, + jsexception: MonoType, +} + +const marshaler_type_symbol = Symbol.for("wasm marshaler_type"); + +export function get_custom_marshalers(signature: JavaScriptMarshalerSignature) + : { cs_to_js_custom_marshalers: Map, js_to_cs_custom_marshalers: Map } { + const args_count = get_signature_argument_count(signature); + const cs_to_js_custom_marshalers = new Map(); + const js_to_cs_custom_marshalers = new Map(); + for (let index = 1; index < args_count + 2; index++) { + const marshaler_js_handle = get_signature_marshaler(signature, index); + if (marshaler_js_handle !== JSHandleNull) { + const mono_type = get_signature_type(signature, index); + const marshaler = get_js_obj(marshaler_js_handle); + assert(marshaler, () => `Unknow marshaler for type ${_get_type_name(mono_type)} at ${index}`); + cs_to_js_custom_marshalers.set(mono_type, marshaler.toJavaScript); + js_to_cs_custom_marshalers.set(mono_type, marshaler.toManaged); + } + } + return { cs_to_js_custom_marshalers, js_to_cs_custom_marshalers }; +} + +export function mono_wasm_register_custom_marshaller(factory_code: MonoString, mono_type: MonoType, js_handle_out: Int32Ptr, is_exception: Int32Ptr): MonoString { + try { + const closure: MarshalerHelpers = { + mono_type, + MONO, BINDING, + get_arg, get_gc_handle, get_js_handle, get_signature_type, get_root_ref, get_arg_type, + set_gc_handle, set_js_handle, set_root_ref, set_arg_type, get_extra_buffer, + get_arg_b8, get_arg_u8, get_arg_i16, get_arg_i32, get_arg_i64, get_arg_f64, get_arg_date, + set_arg_b8, set_arg_u8, set_arg_i16, set_arg_i32, set_arg_i64, set_arg_f64, set_arg_date, + }; + + const js_factory_code = conv_string(factory_code)!; + assert(js_factory_code, "factory code must be provided"); + const type_name = _get_type_name(mono_type); + const bound_js_function_name = "_covertor_" + type_name.replaceAll(".", "_"); + let body = `//# sourceURL=https://mono-wasm.invalid/${bound_js_function_name} \n`; + body += ` const {${Object.keys(closure).toString()}} = closure;\n`; + body += "return " + js_factory_code; + //console.log("-------"); + //console.log(body); + //console.log("-------"); + + const factory = new Function("closure", body); + const marshaller = factory(closure)(); + marshaller[marshaler_type_symbol] = mono_type; + + const js_handle = mono_wasm_get_js_handle(marshaller); + Module.setValue(js_handle_out, js_handle, "i32"); + return MonoStringNull; + } + catch (ex) { + return wrap_error(is_exception, ex); + } +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/memory.ts b/src/mono/wasm/runtime/memory.ts index b61b379e87e8c5..c1257e8d510e77 100644 --- a/src/mono/wasm/runtime/memory.ts +++ b/src/mono/wasm/runtime/memory.ts @@ -48,7 +48,7 @@ export function setU16(offset: _MemOffset, value: number): void { Module.HEAPU16[offset >>> 1] = value; } -export function setU32 (offset: _MemOffset, value: _NumberOrPointer) : void { +export function setU32(offset: _MemOffset, value: _NumberOrPointer): void { Module.HEAPU32[offset >>> 2] = value; } @@ -60,13 +60,15 @@ export function setI16(offset: _MemOffset, value: number): void { Module.HEAP16[offset >>> 1] = value; } -export function setI32 (offset: _MemOffset, value: _NumberOrPointer) : void { +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 export function setI64(offset: _MemOffset, value: number): void { - Module.setValue(offset, value, "i64"); + // TODO patch it into Module in updateGlobalBufferAndViews + const HEAPI64 = new BigInt64Array(Module.HEAP8.buffer); + HEAPI64[offset >>> 3] = BigInt(value); } export function setF32(offset: _MemOffset, value: number): void { @@ -77,7 +79,6 @@ export function setF64(offset: _MemOffset, value: number): void { Module.HEAPF64[offset >>> 3] = value; } - export function getU8(offset: _MemOffset): number { return Module.HEAPU8[offset]; } @@ -104,7 +105,10 @@ export function getI32(offset: _MemOffset): number { // NOTE: Returns a number, not a BigInt. This means values over Number.MAX_SAFE_INTEGER will be corrupted export function getI64(offset: _MemOffset): number { - return Module.getValue(offset, "i64"); + // TODO patch it into Module in updateGlobalBufferAndViews + const HEAPI64 = new BigInt64Array(Module.HEAP8.buffer); + const bigval = HEAPI64[offset >>> 3]; + return Number(BigInt.asIntN(62, bigval)); } export function getF32(offset: _MemOffset): number { diff --git a/src/mono/wasm/runtime/method-binding.ts b/src/mono/wasm/runtime/method-binding.ts index 6dc3daec9f7c42..b6db45803dfd8b 100644 --- a/src/mono/wasm/runtime/method-binding.ts +++ b/src/mono/wasm/runtime/method-binding.ts @@ -18,6 +18,7 @@ import { } from "./method-calls"; import cwraps, { wrap_c_function } from "./cwraps"; import { VoidPtr } from "./types/emscripten"; +import { JavaScriptMarshalerArguments } from "./marshal"; const primitiveConverters = new Map(); const _signature_converters = new Map(); @@ -54,6 +55,11 @@ export function get_method(method_name: string): MonoMethod { export function bind_runtime_method(method_name: string, signature: string): Function { const method = get_method(method_name); + if (signature === "#!") { + return function mono_wasm_invoke_method_dynamic(bound_function_gc_handle: GCHandle, args: JavaScriptMarshalerArguments) { + return cwraps.mono_wasm_invoke_method_dynamic(method, bound_function_gc_handle, args); + }; + } return mono_bind_method(method, null, signature, "BINDINGS_" + method_name); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 49427b3539c07e..a1a429f21472d5 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -14,6 +14,9 @@ import { find_corlib_class } from "./class-loader"; import { VoidPtr, CharPtr } from "./types/emscripten"; import { DotnetPublicAPI } from "./exports"; import { mono_on_abort } from "./run"; +import { initialize_marshalers_to_cs } from "./marshal-to-cs"; +import { KnownTypes } from "./marshal"; +import { initialize_marshalers_to_js } from "./marshal-to-js"; import { mono_wasm_new_root } from "./roots"; export let runtime_is_initialized_resolve: Function; @@ -279,7 +282,7 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi const moduleExt = Module as DotnetModule; - if(!Module.disableDotnet6Compatibility && Module.exports){ + if (!Module.disableDotnet6Compatibility && Module.exports) { // Export emscripten defined in module through EXPORTED_RUNTIME_METHODS // Useful to export IDBFS or other similar types generally exposed as // global types when emscripten is not modularized. @@ -287,10 +290,10 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi const exportName = Module.exports[i]; const exportValue = (Module)[exportName]; - if(exportValue) { + if (exportValue) { globalThisAny[exportName] = exportValue; } - else{ + else { console.warn(`MONO_WASM: The exported symbol ${exportName} could not be found in the emscripten module`); } } @@ -360,6 +363,8 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi } } +export let knownTypes: KnownTypes = null; + export function bindings_lazy_init(): void { if (runtimeHelpers.mono_wasm_bindings_is_ready) return; @@ -377,10 +382,21 @@ export function bindings_lazy_init(): void { runtimeHelpers._unbox_buffer_size = 65536; runtimeHelpers._box_buffer = Module._malloc(runtimeHelpers._box_buffer_size); runtimeHelpers._unbox_buffer = Module._malloc(runtimeHelpers._unbox_buffer_size); + runtimeHelpers._class_boolean = find_corlib_class("System", "Boolean"); + runtimeHelpers._class_byte = find_corlib_class("System", "Byte"); + runtimeHelpers._class_int16 = find_corlib_class("System", "Int16"); runtimeHelpers._class_int32 = find_corlib_class("System", "Int32"); + runtimeHelpers._class_int64 = find_corlib_class("System", "Int64"); runtimeHelpers._class_uint32 = find_corlib_class("System", "UInt32"); + runtimeHelpers._class_float = find_corlib_class("System", "Single"); runtimeHelpers._class_double = find_corlib_class("System", "Double"); - runtimeHelpers._class_boolean = find_corlib_class("System", "Boolean"); + runtimeHelpers._class_string = find_corlib_class("System", "String"); + runtimeHelpers._class_object = find_corlib_class("System", "Object"); + runtimeHelpers._class_intptr = find_corlib_class("System", "IntPtr"); + runtimeHelpers._class_exception = find_corlib_class("System", "Exception"); + runtimeHelpers._class_date_time = find_corlib_class("System", "DateTime"); + runtimeHelpers._class_date_time_offset = find_corlib_class("System", "DateTimeOffset"); + runtimeHelpers._class_task = find_corlib_class("System.Threading.Tasks", "Task"); runtimeHelpers.bind_runtime_method = bind_runtime_method; const bindingAssembly = INTERNAL.BINDING_ASM; @@ -408,6 +424,31 @@ export function bindings_lazy_init(): void { if (!runtimeHelpers.get_call_sig_ref) throw "Can't find GetCallSignatureRef method"; + runtimeHelpers._class_ijs_object = cwraps.mono_wasm_assembly_find_class(binding_module, runtimeHelpers.runtime_namespace, "IJSObject"); + runtimeHelpers._class_js_exception = cwraps.mono_wasm_assembly_find_class(binding_module, runtimeHelpers.runtime_namespace, "JSException"); + + knownTypes = { + bool: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_boolean), + byte: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_byte), + int16: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_int16), + int32: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_int32), + int64: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_int64), + float: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_float), + double: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_double), + intptr: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_intptr), + date_time: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_date_time), + date_time_offset: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_date_time_offset), + string: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_string), + cs_object: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_object), + exception: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_exception), + ijs_object: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_ijs_object), + jsexception: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_js_exception), + task: cwraps.mono_wasm_class_get_type(runtimeHelpers._class_task), + }; + + initialize_marshalers_to_js(); + initialize_marshalers_to_cs(); + _create_primitive_converters(); runtimeHelpers._box_root = mono_wasm_new_root(); diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 2b85e899538164..a5fe429dc06aff 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -143,10 +143,23 @@ export type RuntimeHelpers = { _box_root: any; // A WasmRoot that is guaranteed to contain 0 _null_root: any; + _class_boolean: MonoClass; + _class_byte: MonoClass; + _class_int16: MonoClass; _class_int32: MonoClass; + _class_int64: MonoClass; _class_uint32: MonoClass; + _class_float: MonoClass; _class_double: MonoClass; - _class_boolean: MonoClass; + _class_intptr: MonoClass; + _class_string: MonoClass; + _class_object: MonoClass; + _class_ijs_object: MonoClass; + _class_exception: MonoClass; + _class_date_time: MonoClass; + _class_date_time_offset: MonoClass; + _class_js_exception: MonoClass; + _class_task: MonoClass; mono_wasm_runtime_is_ready: boolean; mono_wasm_bindings_is_ready: boolean;