From e510fbb75f26c553fe11184a1c88b5c84b2c4a0a Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 21 Sep 2022 11:37:33 -0400 Subject: [PATCH] [Java.Interop.Tools.Expressions] Add Java.Interop.Tools.Expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/xamarin/java.interop/issues/616 Context: https://github.com/xamarin/java.interop/issues/14 Context: ff4053cb1e966ebec1c16f97211b9ded936f2707 Context: da5d1b8103bb90f156b93ebac9caa16cfc85764e Context: 4787e0179b349ab5ee0d0dd03d08b694acea4971 Context: 41ba34856ab119ea6e22ab103320180143fdf03d Remember `jnimarshalmethod-gen` (176240d2)? And it's crazy idea to use the System.Linq.Expressions-based custom marshaling infrastructure (ff4053cb, da5d1b81) to generate JNI marshal methods at build/packaging time. And how we had to back burner it because it depended upon System.Reflection.Emit being able to write assemblies to disk, which is a feature that never made it to .NET Core, and is still lacking as of .NET 7? Add `src/Java.Interop.Tools.Expressions`, which contains code which uses Mono.Cecil to compile `Expression` expressions to IL. Then update jnimarshalmethod-gen to use it! Testing this puppy: % mkdir _x % dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll \ bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll \ -v --keeptemp \ --jvm /Library/Java/JavaVirtualMachines/microsoft-11.jdk/Contents/Home/lib/jli/libjli.dylib \ -o _x \ -L bin/TestDebug-net7.0 \ -L /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0 First param is assembly to process; `Java.Interop.Export-Tests.dll` is handy because that's what the `run-test-jnimarshal` target in `Makefile` processes. `-v` is verbose output, `--keeptemp` is keep temporary files (and may be vestigial). `--jvm PATH` is the path to the JVM library to load+use. `-o DIR` is where to place output files; this will create `_x/Java.Interop.Export-Tests.dll`. `-L DIR` adds `DIR` to library resolution paths; this adds `bin/TestDebug/net7.0` (dependencies of `Java.Interop.Export-Tests.dll`) and `Microsoft.NETCore.App/7.0.0-rc.1.22422.12` (net7 libs). What does that *do*? % ikdasm bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll > beg.il % ikdasm _x/Java.Interop.Export-Tests.dll > end.il % git diff --no-index beg.il end.il is a ~2KB diff which shows, paraphrasing greatly: public partial class ExportTest { partial class __<$>_jni_marshal_methods { static IntPtr funcIJavaObject (IntPtr jnienv, IntPtr this) => … // … [JniAddNativeMethodRegistration] static void __RegisterNativeMembers (JniNativeMethodRegistrationArguments args) => … } } internal delegate long _JniMarshal_PP_J (IntPtr jnienv, IntPtr self); // … wherein `ExportTest._<$>_jni_marshal_methods` and the `_JniMarshal*` delegate types are added to the assembly. This also unblocks the desire stated in 4787e017: > For `Java.Base`, @jonpryor wants to support the custom marshaling > infrastructure introduced in 77a6bf86. This would allow types to > participate in JNI marshal method ("connector method") generation > *at runtime*, allowing specialization based on the current set of > types and assemblies. One-off tests: ensure that the generated assembly can be decompiled: % ikdasm bin/TestDebug-net7.0/Java.Interop.Tools.Expressions-Tests-ExpressionAssemblyBuilderTests.dll % monodis bin/TestDebug-net7.0/Java.Interop.Tools.Expressions-Tests-ExpressionAssemblyBuilderTests.dll % ikdasm _x/Java.Interop.Export-Tests.dll % monodis _x/Java.Interop.Export-Tests.dll Re-enable most of `Java.Interop.Export-Tests.dll` for .NET 7; see 41ba3485, which disabled those tests. TODO: we should be able to use `jnimarshalmethod-gen` output as part of the unit tests, a'la c8f3e51a. Unfortunately, `dotnet test` doesn't like the updated assembly. Why? # sanity test: tests run! % % dotnet test bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll … Passed! - Failed: 0, Passed: 17, Skipped: 0, Total: 17, Duration: 110 ms - Java.Interop.Export-Tests.dll (net7.0) # backup! % cp bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll . # …previous jnimarshalmethod-gen.dll invocation… # replace the test assembly % \cp _x/Java.Interop.Export-Tests.dll bin/TestDebug-net7.0 # run tests for updated assembly % dotnet test bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll … No test is available in …/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll. Make sure that test discoverer & executors are registered and platform & framework version settings are appropriate and try again. Additionally, path to test adapters can be specified using /TestAdapterPath command. Example /TestAdapterPath:. Huh? Assembly diff: https://gist.github.com/jonpryor/b8233444f2e51043732bea922f6afc81 --- Java.Interop.sln | 14 + src/Java.Base-ref.cs | 2 +- .../Java.Interop.Export.csproj | 4 +- .../Java.Interop/MarshalMemberBuilder.cs | 24 +- .../Java.Interop.Tools.Expressions.csproj | 27 + .../CecilCompilerExpressionVisitor.cs | 904 ++++++++++++++++++ .../ExpressionAssemblyBuilder.cs | 321 +++++++ .../ExpressionMethodRegistration.cs | 9 + .../RemappingReflectionImporter.cs | 63 ++ .../RemappingReflectionImporterProvider.cs | 18 + .../Java.Interop/MarshalMemberBuilderTest.cs | 18 +- ...ava.Interop.Tools.Expressions-Tests.csproj | 36 + .../ExpressionAssemblyBuilderTests.cs | 423 ++++++++ .../Usings.cs | 1 + tools/jnimarshalmethod-gen/App.cs | 218 +++-- ...oid.Tools.JniMarshalMethodGenerator.csproj | 4 +- 16 files changed, 1977 insertions(+), 109 deletions(-) create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions.csproj create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/CecilCompilerExpressionVisitor.cs create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionMethodRegistration.cs create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporter.cs create mode 100644 src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporterProvider.cs create mode 100644 tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.Expressions-Tests.csproj create mode 100644 tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.ExpressionsTests/ExpressionAssemblyBuilderTests.cs create mode 100644 tests/Java.Interop.Tools.Expressions-Tests/Usings.cs diff --git a/Java.Interop.sln b/Java.Interop.sln index 2abb51d62..d662692ca 100644 --- a/Java.Interop.sln +++ b/Java.Interop.sln @@ -109,6 +109,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Base", "src\Java.Base\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Base-Tests", "tests\Java.Base-Tests\Java.Base-Tests.csproj", "{CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.Expressions", "src\Java.Interop.Tools.Expressions\Java.Interop.Tools.Expressions.csproj", "{1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.Expressions-Tests", "tests\Java.Interop.Tools.Expressions-Tests\Java.Interop.Tools.Expressions-Tests.csproj", "{211BAA88-66B1-41B2-88B2-530DBD8DF702}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5 @@ -308,6 +312,14 @@ Global {CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC}.Release|Any CPU.Build.0 = Release|Any CPU + {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A}.Release|Any CPU.Build.0 = Release|Any CPU + {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Debug|Any CPU.Build.0 = Debug|Any CPU + {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Release|Any CPU.ActiveCfg = Release|Any CPU + {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -360,6 +372,8 @@ Global {11942DE9-AEC2-4B95-87AB-CA707C37643D} = {271C9F30-F679-4793-942B-0D9527CB3E2F} {30DCECA5-16FD-4FD0-883C-E5E83B11565D} = {0998E45F-8BCE-4791-A944-962CD54E2D80} {CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC} = {271C9F30-F679-4793-942B-0D9527CB3E2F} + {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A} = {0998E45F-8BCE-4791-A944-962CD54E2D80} + {211BAA88-66B1-41B2-88B2-530DBD8DF702} = {271C9F30-F679-4793-942B-0D9527CB3E2F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5} diff --git a/src/Java.Base-ref.cs b/src/Java.Base-ref.cs index 973dc50b7..7f42f5710 100644 --- a/src/Java.Base-ref.cs +++ b/src/Java.Base-ref.cs @@ -6408,7 +6408,7 @@ public partial class AccessibleObject : Java.Lang.Object, Java.Interop.IJavaPeer { protected AccessibleObject() { } protected AccessibleObject(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options) { } - public virtual bool Accessible { get { throw null; } set { } } + public virtual bool Accessible { [System.ObsoleteAttribute("deprecated")] get { throw null; } set { } } [System.ComponentModel.EditorBrowsableAttribute(1)] [System.Diagnostics.DebuggerBrowsableAttribute(0)] public override Java.Interop.JniPeerMembers JniPeerMembers { get { throw null; } } diff --git a/src/Java.Interop.Export/Java.Interop.Export.csproj b/src/Java.Interop.Export/Java.Interop.Export.csproj index 4e797ba0b..e9896daef 100644 --- a/src/Java.Interop.Export/Java.Interop.Export.csproj +++ b/src/Java.Interop.Export/Java.Interop.Export.csproj @@ -2,7 +2,7 @@ netstandard2.0;$(DotNetTargetFramework) - 8.0 + 9.0 {B501D075-6183-4E1D-92C9-F7B5002475B1} enable true @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs index 4cb7d6724..351ff035a 100644 --- a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs +++ b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs @@ -84,20 +84,6 @@ public string GetJniMethodSignature (JavaCallableAttribute export, MethodInfo me return export.Signature = GetJniMethodSignature (method); } - string GetTypeSignature (ParameterInfo p) - { - var info = Runtime.TypeManager.GetTypeSignature (p.ParameterType); - if (info.IsValid) - return info.QualifiedReference; - - var marshaler = GetParameterMarshaler (p); - info = Runtime.TypeManager.GetTypeSignature (marshaler.MarshalType); - if (info.IsValid) - return info.QualifiedReference; - - throw new NotSupportedException ("Don't know how to determine JNI signature for parameter type: " + p.ParameterType.FullName + "."); - } - Delegate CreateJniMethodMarshaler (MethodInfo method, JavaCallableAttribute? export, Type? type) { var e = CreateMarshalToManagedExpression (method, export, type); @@ -242,6 +228,7 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav : Expression.Lambda (marshalerType, body, bodyParams); } + // Keep in sync with ExpressionAssemblyBuilder.GetMarshalMethodDelegateType() static Type? GetMarshalerType (Type? returnType, List funcTypeParams, Type? declaringType) { // Too many parameters; does a `_JniMarshal_*` type exist in the type's declaring assembly? @@ -277,6 +264,7 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav static AssemblyBuilder? assemblyBuilder; static ModuleBuilder? moduleBuilder; static Type[]? DelegateCtorSignature; + static Dictionary marshalDelegateTypes; static Type? CreateMarshalDelegateType (string name, Type? returnType, List funcTypeParams) { @@ -290,6 +278,10 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav typeof (object), typeof (IntPtr) }; + marshalDelegateTypes = new (); + } + if (marshalDelegateTypes.TryGetValue (name, out var type)) { + return type; } funcTypeParams.Insert (0, typeof (IntPtr)); funcTypeParams.Insert (0, typeof (IntPtr)); @@ -307,7 +299,9 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav .SetImplementationFlags (ImplAttributes); typeBuilder.DefineMethod ("Invoke", InvokeAttributes, returnType, funcTypeParams.ToArray ()) .SetImplementationFlags (ImplAttributes); - return typeBuilder.CreateTypeInfo (); + var marshalDelType = typeBuilder.CreateTypeInfo (); + marshalDelegateTypes.Add (name, marshalDelType); + return marshalDelType; } } #endif // NET diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions.csproj b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions.csproj new file mode 100644 index 000000000..b8b67337b --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions.csproj @@ -0,0 +1,27 @@ + + + + $(DotNetTargetFramework) + enable + enable + + + + + + $(UtilityOutputFullPath) + + + + + + + + + + + + + + + diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/CecilCompilerExpressionVisitor.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/CecilCompilerExpressionVisitor.cs new file mode 100644 index 000000000..18874c9e3 --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/CecilCompilerExpressionVisitor.cs @@ -0,0 +1,904 @@ +namespace Java.Interop.Tools.Expressions; + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +class CecilCompilerExpressionVisitor : ExpressionVisitor +{ + public CecilCompilerExpressionVisitor (AssemblyDefinition declaringAssembly, MethodBody body, LambdaExpression lambda, VariableDefinitions variables) + { + this.assemblyDef = declaringAssembly; + this.lambda = lambda; + this.body = body; + this.variables = variables; + il = body.GetILProcessor (); + } + + AssemblyDefinition assemblyDef; + MethodBody body; + ILProcessor il; + LambdaExpression lambda; + VariableDefinitions variables; + Dictionary> returnFixups = new (); + + /// + /// Dispatches the expression to one of the more specialized visit methods in this class. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + [return: NotNullIfNotNull("node")] + public override Expression? Visit ( + Expression? node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.Visit [{node?.NodeType.ToString () ?? ""}]: {node}"); + return base.Visit (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitBinary ( + BinaryExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitBinary: {node} [{node.NodeType}]"); + switch (node.NodeType) { + case ExpressionType.Assign: + Visit (node.Right); + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitBinary: visited right"); + if (node.Left is ParameterExpression dest) { + variables [dest].Store (il); + } else { + Console.WriteLine ($"# jonp: don't know where to assign `{node.Left}`!"); + } + break; + case ExpressionType.Equal: + Visit (node.Left); + Visit (node.Right); + il.Emit (OpCodes.Ceq); + break; + default: + Console.WriteLine ($"# jonp: don't know how to emit binary expr {node.NodeType}!"); + base.VisitBinary (node); + break; + } + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitBlock ( + BlockExpression node) + { + // Base method also visits parameter nodes after body; we don't want that. + // https://cs.github.com/dotnet/runtime/blob/9df6ea21007319967975dc9985413bb6518287da/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs#L214 + // return base.VisitBlock (node); + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitBlock: {node}"); + foreach (var e in node.Expressions) { + Visit (e); + } + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitConditional ( + ConditionalExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitConditional: {node}"); + Visit (node.Test); + var startFalse = il.Create (OpCodes.Nop); + var endBranch = il.Create (OpCodes.Nop); + il.Emit (OpCodes.Brfalse, startFalse); + Visit (node.IfTrue); + il.Emit (OpCodes.Br, endBranch); + il.Append (startFalse); + Visit (node.IfFalse); + il.Append (endBranch); + return node; + // return base.VisitConditional (node); + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitConstant ( + ConstantExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitConstant: {node}"); + switch (Type.GetTypeCode (node.Type)) { + case TypeCode.String: + il.Emit (OpCodes.Ldstr, (string?) node.Value); + break; + case TypeCode.Boolean: + if ((bool) node.Value!) { + il.Emit (OpCodes.Ldc_I4_1); + } else { + il.Emit (OpCodes.Ldc_I4_0); + } + break; + case TypeCode.Char: + il.Emit (OpCodes.Ldc_I4, (char) node.Value!); + break; + case TypeCode.SByte: + il.Emit (OpCodes.Ldc_I4_S, (sbyte) node.Value!); + break; + case TypeCode.Byte: + il.Emit (OpCodes.Ldc_I4, (byte) node.Value!); + break; + case TypeCode.Int16: + il.Emit (OpCodes.Ldc_I4, (short) node.Value!); + break; + case TypeCode.Int32: + il.Emit (OpCodes.Ldc_I4, (int) node.Value!); + break; + case TypeCode.Int64: + il.Emit (OpCodes.Ldc_I8, (long) node.Value!); + break; + case TypeCode.Single: + il.Emit (OpCodes.Ldc_R4, (float) node.Value!); + break; + case TypeCode.Double: + il.Emit (OpCodes.Ldc_R8, (double) node.Value!); + break; + case TypeCode.UInt16: + il.Emit (OpCodes.Ldc_I4, (short) node.Value!); + break; + case TypeCode.UInt32: + il.Emit (OpCodes.Ldc_I4, (int) node.Value!); + break; + case TypeCode.UInt64: + il.Emit (OpCodes.Ldc_I8, (int) node.Value!); + break; + case TypeCode.Object: + if (node.Type == typeof (Type)) { + Console.WriteLine ($"# jonp: TODO load type {node.Value}"); + break; + } else if (node.Value == null) { + Console.WriteLine ($"# jonp: TODO ldnull {node.Value}"); + il.Emit (OpCodes.Ldnull); + break; + } + goto default; + default: + Console.WriteLine ($"# jonp: don't know how to deal with constant with value `{node}` NodeType `{node.NodeType}` Type `{node.Type}` typecode {Type.GetTypeCode (node.Type)}"); + break; + // throw new NotSupportedException (); + } + return node; + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitDebugInfo ( + DebugInfoExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitDebugInfo: {node}"); + return base.VisitDebugInfo (node); + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitDefault ( + DefaultExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitDefault: {node}"); + return base.VisitDefault (node); + } + + /// + /// Visits the children of the extension expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + /// + /// This can be overridden to visit or rewrite specific extension nodes. + /// If it is not overridden, this method will call , + /// which gives the node a chance to walk its children. By default, + /// will try to reduce the node. + /// + protected override Expression VisitExtension ( + Expression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitExtension: {node}"); + return base.VisitExtension (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitGoto ( + GotoExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitGoto: {node}"); + if (node.Kind != GotoExpressionKind.Return || node.Type == typeof (void)) { + return base.VisitGoto (node); + } + Visit (node.Value); + variables.ReturnValue?.Store (il); + il.Emit (OpCodes.Ret); + List fixups; + if (!returnFixups.TryGetValue (node.Target, out fixups)) { + returnFixups.Add (node.Target, fixups = new ()); + } + fixups.Add (il.Body.Instructions.Last ()); + Console.WriteLine ($"# jonp: adding fixup for goto `{node}` at index {il.Body.Instructions.Count-1}"); + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitInvocation ( + InvocationExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitInvocation: {node}"); + return base.VisitInvocation (node); + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + [return: NotNullIfNotNull("node")] + protected override LabelTarget? VisitLabelTarget ( + LabelTarget? node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitLabelTarget: {node}"); + if (node != null) { + GetFixupsForLabelTarget (node).Add (il.Body.Instructions.Last ()); + } + return base.VisitLabelTarget (node); + } + + List GetFixupsForLabelTarget (LabelTarget target) + { + List fixups; + if (!returnFixups.TryGetValue (target, out fixups)) { + returnFixups.Add (target, fixups = new ()); + } + return fixups; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitLabel ( + LabelExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitLabel: {node}"); + var target = il.Body.Instructions.Last (); + if (returnFixups.TryGetValue (node.Target, out var fixups)) { + foreach (var replace in fixups) { + replace.OpCode = OpCodes.Leave; + replace.Operand = target; + } + } + return base.VisitLabel (node); + } + + /// + /// Visits the children of the . + /// + /// The type of the delegate. + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitLambda(Expression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitLambda: {node}"); + return Visit (node.Body); + // Base method also visits parameter nodes after body; we don't want that. + // return base.VisitLambda (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitLoop ( + LoopExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitLoop: {node}"); + return base.VisitLoop (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitMember ( + MemberExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMember: {node}"); + base.VisitMember (node); + switch (node.Member.MemberType) { + case System.Reflection.MemberTypes.Field: + var field = (System.Reflection.FieldInfo) node.Member; + il.Emit ( + field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, + assemblyDef.MainModule.ImportReference (field)); + break; + case System.Reflection.MemberTypes.Property: + var property = (System.Reflection.PropertyInfo) node.Member; + var getter = property.GetGetMethod (); + il.Emit ( + getter!.IsStatic ? OpCodes.Call: OpCodes.Callvirt, + assemblyDef.MainModule.ImportReference (getter)); + break; + default: + throw new NotSupportedException ($"How do I visit `{node.Member.MemberType}`? {node}"); + } + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitIndex ( + IndexExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitIndex: {node}"); + return base.VisitIndex (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitMethodCall ( + MethodCallExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMethodCall: {node}"); + +/* + foreach (var a in node.Arguments) { + switch (a.NodeType) { + case ExpressionType.Constant: + var c = (ConstantExpression) a; + switch (Type.GetTypeCode (a.Type)) { + case TypeCode.String: + il.Emit (OpCodes.Ldstr, (string) c.Value); + break; + case TypeCode.Int32: + il.Emit (OpCodes.Ldc_I4, (int) c.Value); + break; + default: + // throw new NotSupportedException (); + break; + } + break; + default: + // throw new NotSupportedException (); + break; + } + } + Console.WriteLine ($"# jonp: node.Method? {node.Method != null}"); + var methodRef = assemblyDef.MainModule.ImportReference (node.Method); + Console.WriteLine ($"# jonp: methodRef? {methodRef != null}"); + il.Emit (OpCodes.Call, methodRef); + + Console.WriteLine ($"# jonp: begin base.VisitMethodCall {node}"); + base.VisitMethodCall (node); + Console.WriteLine ($"# jonp: end base.VisitMethodCall {node}"); +*/ + // Visit (node.Object); + base.VisitMethodCall (node); + il.Emit (OpCodes.Call, assemblyDef.MainModule.ImportReference (node.Method)); + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitNewArray ( + NewArrayExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitNewArray: {node}"); + return base.VisitNewArray (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitNew ( + NewExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitNew: {node}"); + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitNew: ctor={node.Constructor} {node.Constructor != null}"); + base.VisitNew (node); + if (node.Constructor == null && node.Type.IsValueType) { + il.Emit (OpCodes.Initobj, assemblyDef.MainModule.ImportReference (node.Type)); + } else { + il.Emit (OpCodes.Call, assemblyDef.MainModule.ImportReference (node.Constructor)); + } + return node; + } + + /// + /// Visits the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitParameter ( + ParameterExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitParameter: {node}"); + + variables [node].Load (il); + + return node; + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitRuntimeVariables ( + RuntimeVariablesExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitRuntimeVariables: {node}"); + return base.VisitRuntimeVariables (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override SwitchCase VisitSwitchCase ( + SwitchCase node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitSwitchCase: {node}"); + return base.VisitSwitchCase (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitSwitch ( + SwitchExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitSwitch: {node}"); + return base.VisitSwitch (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override CatchBlock VisitCatchBlock ( + CatchBlock node) + { + // On entry, IL stream should assume that there is an Exception type on the evaluation stack. + + Console.WriteLine($"# jonp: CecilCompilerExpressionVisitor.VisitCatchBlock: {node}"); + + var startCatchBlock = il.Body.Instructions.Count; + var handlerDef = new ExceptionHandler (ExceptionHandlerType.Catch) { + TryStart = TryStart, + }; + body.ExceptionHandlers.Add (handlerDef); + + if (node.Filter != null) { + EmitCatchFilter (node); + handlerDef.HandlerType = ExceptionHandlerType.Filter; + handlerDef.FilterStart = il.Body.Instructions [startCatchBlock]; + startCatchBlock = il.Body.Instructions.Count; + } else if (node.Test != null) { + handlerDef.CatchType = assemblyDef.MainModule.ImportReference (node.Test); + } + + if (node.Variable != null) { + variables [node.Variable!].Store (il); + } else { + il.Emit (OpCodes.Pop); + } + + Visit (node.Body); + + handlerDef.HandlerStart = il.Body.Instructions [startCatchBlock]; + + return node; + } + + void EmitCatchFilter (CatchBlock node) + { + Instruction? fixupStartFilter = null; + Instruction? fixupEndFilter = null; + + if (node.Test != null) { + il.Emit (OpCodes.Isinst, assemblyDef.MainModule.ImportReference (node.Test)); + il.Emit (OpCodes.Dup); + il.Emit (OpCodes.Brtrue_S, il.Body.Instructions.Last ()); + fixupStartFilter = il.Body.Instructions.Last (); + il.Emit (OpCodes.Pop); + il.Emit (OpCodes.Ldc_I4_0); + il.Emit (OpCodes.Br_S, il.Body.Instructions.Last ()); + fixupEndFilter = il.Body.Instructions.Last (); + } + + if (node.Variable != null) { + variables [node.Variable!].Store (il); + } else { + il.Emit (OpCodes.Pop); + } + + if (fixupStartFilter != null) { + fixupStartFilter.Operand = il.Body.Instructions.Last (); + } + + Visit (node.Filter); + + // node.Filter is assumed to leave a "boolean" on the eval stack; convert to an int + il.Emit (OpCodes.Ldc_I4_0); + il.Emit (OpCodes.Cgt_Un); + + il.Emit (OpCodes.Endfilter); + + if (fixupEndFilter != null) { + fixupEndFilter.Operand = il.Body.Instructions.Last (); + } + } + + Instruction? TryStart; + List? FixupLeaveOffsets; + + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitTry ( + TryExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitTry: {node}"); + + var prevTryStart = TryStart; + var pFixupLeaveOffsets = FixupLeaveOffsets; + try { + var startTryBlock = il.Body.Instructions.Count; + FixupLeaveOffsets = new (); + + Visit (node.Body); + EmitLeave (); + TryStart = il.Body.Instructions [startTryBlock]; + + Visit (node.Handlers, VisitCatchBlock); + + if (node.Finally != null) { + var startFinallyBlock = il.Body.Instructions.Count; + Visit (node.Finally); + il.Emit (OpCodes.Endfinally); + + var finallyDef = new ExceptionHandler (ExceptionHandlerType.Finally) { + TryStart = TryStart, + HandlerStart = il.Body.Instructions [startFinallyBlock], + }; + body.ExceptionHandlers.Add (finallyDef); + } + + // Visit (node.Fault); + + // ECMA 335 Partition X § 19 Exception Handling + // HandlerBlock ::= `handler` Label to Label + // Handler range is from first label ***prior to*** second (emphasis @jonpryor) + // Therefore we need to append `NOP` to the IL stream so that the fixupTarget is + // one-past-the-end, as nothing afterward has yet been emitted. + + il.Emit (OpCodes.Nop); + var fixupTarget = il.Body.Instructions.Last (); + + for (int i = 0; i < (body.ExceptionHandlers.Count-1); ++i) { + var c = body.ExceptionHandlers [i]; + var n = body.ExceptionHandlers [i+1]; + c.TryEnd = c.FilterStart ?? c.HandlerStart; + c.HandlerEnd = n.FilterStart ?? n.HandlerStart; + } + if (body.ExceptionHandlers.Count > 0) { + var f = body.ExceptionHandlers [body.ExceptionHandlers.Count-1]; + f.TryEnd = f.HandlerStart; + f.HandlerEnd = fixupTarget; + } + foreach (var fixup in FixupLeaveOffsets) { + fixup.Operand = fixupTarget; + } + } + finally { + TryStart = prevTryStart; + FixupLeaveOffsets = pFixupLeaveOffsets; + } + + return node; + } + + void EmitLeave () + { + // keep in sync w/ VisitGoto() + // Prevent multiple `leave OFFSET`s in the output + if (il.Body.Instructions.Last ().OpCode.Code != Code.Ret) { + il.Emit (OpCodes.Leave, il.Body.Instructions.Last ()); + FixupLeaveOffsets?.Add (il.Body.Instructions.Last ()); + } + } + +#if false + protected override Expression VisitTry ( + TryExpression node) + { + var tryStart = this.tryStart; + this.tryStart = il. + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitTry: {node}"); + // base.VisitTry (node); + // Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitTry-END: {node}"); + var fixupLeaveOffsets = new List (); + + var tryStart = EmitBlockBoundary (node.Body); + foreach (var handler in node.Handlers) { + var catchIndex = il.Body.Instructions.Count; + variables [handler.Variable].Store (il); + EmitBlockBoundary (handler.Body); + + var catchStart = il.Body.Instructions [catchIndex]; + + var handlerDef = new ExceptionHandler (ExceptionHandlerType.Catch) { + TryStart = tryStart, + HandlerStart = catchStart, + CatchType = assemblyDef.MainModule.ImportReference (handler.Test), + }; + body.ExceptionHandlers.Add (handlerDef); + } + if (node.Finally != null) { + var finallyStart = EmitBlockBoundary (node.Finally, emitLeave: false); + il.Emit (OpCodes.Endfinally); + + var finallyDef = new ExceptionHandler (ExceptionHandlerType.Finally) { + TryStart = tryStart, + HandlerStart = finallyStart, + }; + body.ExceptionHandlers.Add (finallyDef); + } + + il.Emit (OpCodes.Nop); + var fixupTarget = il.Body.Instructions.Last (); + + // ECMA 335 Partition X § 19 Exception Handling + // HandlerBlock ::= `handler` Label to Label + // Handler range is from first label ***prior to*** second (emphasis @jonpryor) + + for (int i = 0; i < (body.ExceptionHandlers.Count-1); ++i) { + var c = body.ExceptionHandlers [i]; + var n = body.ExceptionHandlers [i+1]; + c.TryEnd = c.HandlerStart; + c.HandlerEnd = n.HandlerStart; + } + if (body.ExceptionHandlers.Count > 0) { + var f = body.ExceptionHandlers [body.ExceptionHandlers.Count-1]; + f.TryEnd = f.HandlerStart; + f.HandlerEnd = fixupTarget; + } + + foreach (var fixup in fixupLeaveOffsets) { + fixup.Operand = fixupTarget; + } + + this.tryStart = tryStart; + + return node; + // return base.VisitTry (node); + + Instruction EmitBlockBoundary (Expression block, bool emitLeave = true) + { + var startIndex = il.Body.Instructions.Count; + Visit (block); + if (il.Body.Instructions.Count == startIndex) { + il.Emit (OpCodes.Nop); + } + var startInstruction = il.Body.Instructions [startIndex]; + + // keep in sync w/ VisitGoto() + // Prevent multiple `leave OFFSET`s in the output + if (emitLeave && il.Body.Instructions.Last ().OpCode.Code != Code.Ret) { + il.Emit (OpCodes.Leave, startInstruction); + fixupLeaveOffsets.Add (il.Body.Instructions.Last ()); + } + + return startInstruction; + } + } +#endif + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitTypeBinary ( + TypeBinaryExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitTypeBinary: {node}"); + return base.VisitTypeBinary (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitUnary ( + UnaryExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitUnary: {node}"); + return base.VisitUnary (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitMemberInit ( + MemberInitExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMemberInit: {node}"); + return base.VisitMemberInit (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitListInit ( + ListInitExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitListInit: {node}"); + return base.VisitListInit (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override ElementInit VisitElementInit ( + ElementInit node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitElementInit: {node}"); + return base.VisitElementInit (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override MemberBinding VisitMemberBinding ( + MemberBinding node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMemberBinding: {node}"); + return base.VisitMemberBinding (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override MemberAssignment VisitMemberAssignment ( + MemberAssignment node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMemberAssignment: {node}"); + return base.VisitMemberAssignment (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override MemberMemberBinding VisitMemberMemberBinding ( + MemberMemberBinding node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMemberMemberBinding: {node}"); + return base.VisitMemberMemberBinding (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override MemberListBinding VisitMemberListBinding ( + MemberListBinding node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitMemberListBinding: {node}"); + return base.VisitMemberListBinding (node); + } + + /// + /// Visits the children of the . + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; + /// otherwise, returns the original expression. + protected override Expression VisitDynamic ( + DynamicExpression node) + { + Console.WriteLine ($"# jonp: CecilCompilerExpressionVisitor.VisitDynamic: {node}"); + return base.VisitDynamic (node); + } +} diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs new file mode 100644 index 000000000..0d0cb5811 --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Text; + +using Java.Interop; +using Java.Interop.Tools.Diagnostics; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Java.Interop.Tools.Expressions; + +public class ExpressionAssemblyBuilder { + + public ExpressionAssemblyBuilder (AssemblyDefinition declaringAssemblyDefinition, Action? logger = null) + { + DeclaringAssemblyDefinition = declaringAssemblyDefinition; + Logger = logger ?? Diagnostic.CreateConsoleLogger (); + } + + public AssemblyDefinition DeclaringAssemblyDefinition {get;} + public Action Logger {get;} + + public MethodDefinition Compile (LambdaExpression expression) + { + var mmDef = CreateMethodDefinition (DeclaringAssemblyDefinition, expression); + var decls = new VariableDefinitions (DeclaringAssemblyDefinition, mmDef, expression); + var mmBody = mmDef.Body; + + var v = new CecilCompilerExpressionVisitor (DeclaringAssemblyDefinition, mmBody, expression, decls); + v.Visit (expression); + + var il = mmBody.GetILProcessor (); + decls.ReturnValue?.Load (il); + il.Emit (OpCodes.Ret); + + return mmDef; + } + + static MethodDefinition CreateMethodDefinition (AssemblyDefinition declaringAssembly, LambdaExpression expression) + { + var mmDef = new MethodDefinition ( + name: "@CHANGE-ME@", + attributes: Mono.Cecil.MethodAttributes.Static | Mono.Cecil.MethodAttributes.Private | Mono.Cecil.MethodAttributes.HideBySig, + returnType: declaringAssembly.MainModule.ImportReference (expression.ReturnType) + ); + return mmDef; + } + + public MethodDefinition CreateRegistrationMethod (IList methods) + { + var registrations = new MethodDefinition ( + name: "__RegisterNativeMembers", + attributes: MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig, + returnType: DeclaringAssemblyDefinition.MainModule.TypeSystem.Void + ); + var ctor = typeof (JniAddNativeMethodRegistrationAttribute).GetConstructor (Type.EmptyTypes); + var attr = new CustomAttribute (DeclaringAssemblyDefinition.MainModule.ImportReference (ctor)); + registrations.CustomAttributes.Add (attr); + + var args = new ParameterDefinition ("args", default, DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (JniNativeMethodRegistrationArguments))); + registrations.Parameters.Add (args); + + var arrayType = DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (JniNativeMethodRegistration [])); + + var array = new VariableDefinition (arrayType); + registrations.Body.Variables.Add (array); + + var il = registrations.Body.GetILProcessor (); + il.Emit (OpCodes.Ldc_I4, methods.Count); + il.Emit (OpCodes.Newarr, DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (JniNativeMethodRegistration))); + il.Emit (OpCodes.Stloc_0); + + var JniNativeMethodRegistration_ctor = typeof (JniNativeMethodRegistration).GetConstructor (new [] { typeof (string), typeof (string), typeof (Delegate) }); + var jnmr_ctor = DeclaringAssemblyDefinition.MainModule.ImportReference (JniNativeMethodRegistration_ctor); + + for (int i = 0; i < methods.Count; i++) { + var delegateCtor = GetMarshalMethodDelegateCtor (methods [i].MarshalMethodDefinition); + + il.Emit (OpCodes.Ldloc_0); // args + il.Emit (OpCodes.Ldc_I4, i); // index of `args` to set + + // new JniNativeMethodRegistration (JniName, JniSignature, new _JniMarshal_PP… (MarshalMethodDefinition)) + il.Emit (OpCodes.Ldstr, methods [i].JniName); + il.Emit (OpCodes.Ldstr, methods [i].JniSignature); + il.Emit (OpCodes.Ldnull); + il.Emit (OpCodes.Ldftn, methods [i].MarshalMethodDefinition); + il.Emit (OpCodes.Newobj, delegateCtor); + il.Emit (OpCodes.Newobj, jnmr_ctor); + + il.Emit (OpCodes.Stelem_Any, arrayType); // args [i] = new JniNativeMethodRegistration (…) + } + + Action> addRegistrations = new JniNativeMethodRegistrationArguments ().AddRegistrations; + il.Emit (OpCodes.Ldarg_0); + il.Emit (OpCodes.Ldloc_0); + il.Emit (OpCodes.Call, DeclaringAssemblyDefinition.MainModule.ImportReference (addRegistrations.Method)); + il.Emit (OpCodes.Ret); + + + return registrations; + } + + // Keep in sync w/ MarshalMemberBuilder.GetMarshalerType() + MethodReference GetMarshalMethodDelegateCtor (MethodDefinition method) + { + // Too many parameters; does a `_JniMarshal_*` type exist in the type's declaring assembly? + var delegateName = new StringBuilder (); + delegateName.Append ("_JniMarshal_PP"); + + for (int i = 2; i < method.Parameters.Count; i++) { + delegateName.Append (GetJniMarshalDelegateParameterIdentifier (method.Parameters [i].ParameterType)); + } + delegateName.Append ("_"); + delegateName.Append (GetJniMarshalDelegateParameterIdentifier (method.ReturnType)); + + var delegateDef = DeclaringAssemblyDefinition.MainModule.GetType (delegateName.ToString ()); + if (delegateDef != null) { + return delegateDef.Methods.First (m => m.Name == ".ctor"); + } + delegateDef = new TypeDefinition ( + @namespace: "", + name: delegateName.ToString (), + attributes: TypeAttributes.Class | TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass + ); + delegateDef.BaseType = DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (MulticastDelegate)); + + var delegateCtor = new MethodDefinition ( + name: ".ctor", + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName , + returnType: DeclaringAssemblyDefinition.MainModule.TypeSystem.Void + ); + delegateCtor.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + delegateCtor.Parameters.Add (new ParameterDefinition ("object", default, DeclaringAssemblyDefinition.MainModule.TypeSystem.Object)); + delegateCtor.Parameters.Add (new ParameterDefinition ("method", default, DeclaringAssemblyDefinition.MainModule.TypeSystem.IntPtr)); + delegateDef.Methods.Add (delegateCtor); + + var invoke = new MethodDefinition ( + name: "Invoke", + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot| MethodAttributes.Virtual, + returnType: method.ReturnType + ); + invoke.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + foreach (var p in method.Parameters) { + invoke.Parameters.Add (new ParameterDefinition (p.Name, p.Attributes, p.ParameterType)); + } + delegateDef.Methods.Add (invoke); + + // BeginInvoke() EndInvoke() appear to be automagically added, in that ikdasm shows they exist, even if we don't add them. + + DeclaringAssemblyDefinition.MainModule.Types.Add (delegateDef); + + return delegateCtor; + } + + char GetJniMarshalDelegateParameterIdentifier (TypeReference type) + { + switch (type?.FullName) { + case "System.Boolean": return 'Z'; + case "System.Byte": return 'B'; + case "System.SByte": return 'B'; + case "System.Char": return 'C'; + case "System.Int16": return 'S'; + case "System.UInt16": return 's'; + case "System.Int32": return 'I'; + case "System.UInt32": return 'i'; + case "System.Int64": return 'J'; + case "System.UInt64": return 'j'; + case "System.Single": return 'F'; + case "System.Double": return 'D'; + case null: + case "System.Void": return 'V'; + default: return 'L'; + } + } + + + public void Write (string path) + { + DeclaringAssemblyDefinition.Write (path); + } +} + +sealed class VariableInfo { + public VariableInfo (Action load, Action store) + { + Load = load; + Store = store; + } + + public Action Load; + public Action Store; +} + +sealed class VariableDefinitions { + + Dictionary variables = new (); + + public VariableDefinitions (AssemblyDefinition declaringAssembly, MethodDefinition declaringMethod, LambdaExpression expression) + { + for (int i = 0; i < expression.Parameters.Count; ++i) { + var c = expression.Parameters [i]; + var d = new ParameterDefinition (c.Name, default, declaringAssembly.MainModule.ImportReference (c.Type)); + declaringMethod.Parameters.Add (d); + + VariableInfo v; + + switch (i) { + case 0: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_0), il => il.Emit (OpCodes.Starg, 0)); + break; + case 1: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_1), il => il.Emit (OpCodes.Starg, 1)); + break; + case 2: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_2), il => il.Emit (OpCodes.Starg, 2)); + break; + case 3: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_3), il => il.Emit (OpCodes.Starg, 3)); + break; + default: + int x = i; + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg, x), il => il.Emit (OpCodes.Starg, x)); + break; + } + variables [c] = v; + } + FillVariables (declaringAssembly, declaringMethod, expression); + } + + public VariableInfo? ReturnValue {get; private set;} + + public VariableInfo this [ParameterExpression e] { + get => variables [e]; + } + + void FillVariables ( + AssemblyDefinition declaringAssembly, + MethodDefinition declaringMethod, + Expression e) + { + var variableVisitor = new VariableExpressionVisitor (variables.Keys); + variableVisitor.Visit (e); + + Console.WriteLine ($"# jonp: filling {variableVisitor.Variables.Count} variables"); + for (int i = 0; i < variableVisitor.Variables.Count; ++i) { + var c = variableVisitor.Variables [i]; + var d = new VariableDefinition (declaringAssembly.MainModule.ImportReference (c.Type)); + declaringMethod.Body.Variables.Add (d); + + VariableInfo v; + + switch (i) { + case 0: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_0), il => il.Emit (OpCodes.Stloc_0)); + break; + case 1: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_1), il => il.Emit (OpCodes.Stloc_1)); + break; + case 2: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_2), il => il.Emit (OpCodes.Stloc_2)); + break; + case 3: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_3), il => il.Emit (OpCodes.Stloc_3)); + break; + default: + var x = i; + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc, x), il => il.Emit (OpCodes.Stloc, x)); + break; + } + variables [c] = v; + if (c == variableVisitor.ReturnValue) { + ReturnValue = v; + } + Console.WriteLine ($"# jonp: FillVariables: local var {c.Name} is index {i}"); + } + } +} + +class VariableExpressionVisitor : ExpressionVisitor { + + public VariableExpressionVisitor (ICollection arguments) + { + this.arguments = arguments; + } + + ICollection arguments; + + public List Variables = new (); + public ParameterExpression? ReturnValue; + + protected override Expression VisitGoto ( + GotoExpression node) + { + Console.WriteLine ($"# jonp: VariableExpressionVisitor.Goto: {node}"); + if (node.Kind != GotoExpressionKind.Return) { + return base.VisitGoto (node); + } + if (node.Type == typeof (void)) { + return base.VisitGoto (node); + } + if (ReturnValue != null) { + return base.VisitGoto (node); + } + var p = Expression.Parameter (node.Type, "__goto.Return.Temporary"); + Variables.Add (p); + ReturnValue = p; + return base.VisitGoto (node); + } + + protected override Expression VisitParameter ( + ParameterExpression node) + { + if (!arguments.Contains (node) && !Variables.Contains (node)) { + Variables.Add (node); + } + return node; + } +} diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionMethodRegistration.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionMethodRegistration.cs new file mode 100644 index 000000000..91ae63fe2 --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionMethodRegistration.cs @@ -0,0 +1,9 @@ +using System; + +using Mono.Cecil; + +namespace Java.Interop.Tools.Expressions; + +public record ExpressionMethodRegistration (string JniName, string JniSignature, MethodDefinition MarshalMethodDefinition) +{ +} diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporter.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporter.cs new file mode 100644 index 000000000..fef5b25df --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporter.cs @@ -0,0 +1,63 @@ +using System; +using System.Reflection; +using Mono.Cecil; + +namespace Java.Interop.Tools.Expressions; + +class RemappingReflectionImporter : DefaultReflectionImporter +{ + static RemappingReflectionImporter () + { + var rp = new ReaderParameters { + ReadingMode = ReadingMode.Deferred, + InMemory = true, + ReadSymbols = false, + ReadWrite = false, + ApplyWindowsRuntimeProjections = false, + + }; + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location) ?? throw new InvalidOperationException ("Should not be reached; can't find assembly containing System.Object?!"); + var runtimePath = Path.Combine (runtimeDir, "System.Runtime.dll"); + if (!File.Exists (runtimePath)) { + throw new InvalidOperationException ($"Should not be reached; unable to find `System.Runtime.dll` at `{runtimePath}`."); + } + using var runtime = AssemblyDefinition.ReadAssembly (runtimePath, rp); + System_RuntimeAssemblyNameReference = new AssemblyNameReference (runtime.Name.Name, runtime.Name.Version) { + PublicKeyToken = runtime.Name.PublicKeyToken, + Culture = runtime.Name.Culture, + HashAlgorithm = runtime.Name.HashAlgorithm, + }; + } + + static readonly AssemblyNameReference System_RuntimeAssemblyNameReference; + + public RemappingReflectionImporter (ModuleDefinition module) + : base (module) + { + } + + bool IsSameAssembly (Assembly assembly) => + base.module.Mvid == assembly.ManifestModule.ModuleVersionId; + + protected override IMetadataScope ImportScope (Type type) + { + // Needed to prevent adding a self-referencing reference, i.e. `Foo.dll` references `Foo.dll`. + if (!IsSameAssembly (type.Assembly)) { + return base.ImportScope (type); + } + return base.module; + } + + public override AssemblyNameReference ImportReference (AssemblyName name) + { + switch (name.Name) { + case "System.Private.CoreLib": + if (!base.module.AssemblyReferences.Any (r => r.FullName == System_RuntimeAssemblyNameReference.FullName)) { + base.module.AssemblyReferences.Add (System_RuntimeAssemblyNameReference); + } + return System_RuntimeAssemblyNameReference; + } + return base.ImportReference (name); + } +} + diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporterProvider.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporterProvider.cs new file mode 100644 index 000000000..c729ab53e --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/RemappingReflectionImporterProvider.cs @@ -0,0 +1,18 @@ +using System; + +using Mono.Cecil; + +namespace Java.Interop.Tools.Expressions; + +public class RemappingReflectionImporterProvider : IReflectionImporterProvider +{ + public RemappingReflectionImporterProvider () + { + } + + public IReflectionImporter GetReflectionImporter (ModuleDefinition module) + { + return new RemappingReflectionImporter (module); + } +} + diff --git a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs index 3d117c508..4612dd932 100644 --- a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs +++ b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs @@ -12,7 +12,6 @@ namespace Java.InteropTests { -#if !NET [TestFixture] class MarshalMemberBuilderTest : JavaVMFixture { @@ -27,11 +26,11 @@ public void AddExportMethods () Assert.AreEqual ("action", methods [0].Name); Assert.AreEqual ("()V", methods [0].Signature); - Assert.IsTrue (methods [0].Marshaler is Action); + Assert.AreEqual ("_JniMarshal_PP_V", methods [0].Marshaler.GetType ().FullName); - Assert.AreEqual ("staticAction", methods [1].Name); - Assert.AreEqual ("()V", methods [1].Signature); - Assert.IsTrue (methods [1].Marshaler is Action); + Assert.AreEqual ("staticAction", methods [1].Name); + Assert.AreEqual ("()V", methods [1].Signature); + Assert.AreEqual ("_JniMarshal_PP_V", methods [1].Marshaler.GetType ().FullName); var m = t.GetStaticMethod ("testStaticMethods", "()V"); JniEnvironment.StaticMethods.CallStaticVoidMethod (t.PeerReference, m); @@ -201,6 +200,12 @@ static void CheckExpression (LambdaExpression expression, string memberName, Typ { Console.WriteLine ("## member: {0}", memberName); Console.WriteLine (expression.ToCSharpCode ()); + Assert.AreEqual (expectedBody, expression.ToCSharpCode ()); +#if NET + // TODO: Use src/Java.Interop.Tools.Expressions to compile `expression` + // and use the "IL decompiler" in tests/Java.Interop.Tools.Expressions-Tests + // to verify the expected IL +#else var da = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("dyn"), // call it whatever you want System.Reflection.Emit.AssemblyBuilderAccess.Save, @@ -216,10 +221,10 @@ static void CheckExpression (LambdaExpression expression, string memberName, Typ expression.CompileToMethod (mb); dt.CreateType(); Assert.AreEqual (expressionType, expression.Type); - Assert.AreEqual (expectedBody, expression.ToCSharpCode ()); #if !__ANDROID__ da.Save (_name); #endif // !__ANDROID__ +#endif // !NET } [Test] @@ -556,5 +561,4 @@ public void CreateConstructActivationPeerExpression () }}"); } } -#endif // !NET } diff --git a/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.Expressions-Tests.csproj b/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.Expressions-Tests.csproj new file mode 100644 index 000000000..dfc8b2a90 --- /dev/null +++ b/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.Expressions-Tests.csproj @@ -0,0 +1,36 @@ + + + + $(DotNetTargetFramework) + Java.Interop.Tools.ExpressionsTests + enable + enable + false + + + + + + $(TestOutputFullPath) + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.ExpressionsTests/ExpressionAssemblyBuilderTests.cs b/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.ExpressionsTests/ExpressionAssemblyBuilderTests.cs new file mode 100644 index 000000000..21d5cc5e6 --- /dev/null +++ b/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.ExpressionsTests/ExpressionAssemblyBuilderTests.cs @@ -0,0 +1,423 @@ +namespace Java.Interop.Tools.ExpressionsTests; + +using System.IO; +using System.Linq.Expressions; +using System.Text; + +using Java.Interop.Tools.Expressions; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +using Mono.Linq.Expressions; + +[TestFixture] +public class ExpressionAssemblyBuilderTests +{ + static readonly string AssemblyModuleBaseName; + + static ExpressionAssemblyBuilderTests () + { + AssemblyModuleBaseName = typeof (ExpressionAssemblyBuilderTests).Assembly.GetName ().Name + + "-" + + nameof (ExpressionAssemblyBuilderTests); + } + + ExpressionAssemblyBuilder ExpressionAssemblyBuilder; + AssemblyDefinition AssemblyDefinition; + TypeDefinition TypeDefinition; + + [OneTimeSetUp] + public void InitializeTestEnvironment () + { + var moduleParams = new ModuleParameters { + ReflectionImporterProvider = new RemappingReflectionImporterProvider (), + Kind = ModuleKind.Dll, + }; + AssemblyDefinition = AssemblyDefinition.CreateAssembly ( + assemblyName: new AssemblyNameDefinition (AssemblyModuleBaseName, new Version (0, 0, 0, 0)), + moduleName: AssemblyModuleBaseName + ".dll", + parameters: moduleParams + ); + TypeDefinition = new TypeDefinition ( + @namespace: "Example", + name: "Output", + attributes: TypeAttributes.Public | TypeAttributes.Sealed + ); + TypeDefinition.BaseType = AssemblyDefinition.MainModule.ImportReference (typeof (object)); + AssemblyDefinition.MainModule.Types.Add (TypeDefinition); + + ExpressionAssemblyBuilder = new ExpressionAssemblyBuilder (AssemblyDefinition); + } + + [OneTimeTearDown] + public void TearDownTestEnvironment () + { + var path = Path.GetDirectoryName (typeof (ExpressionAssemblyBuilderTests).Assembly.Location); + ExpressionAssemblyBuilder.Write (Path.Combine (path, AssemblyModuleBaseName + ".dll")); + } + + void AddMethod (MethodDefinition method, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + method.Name = methodName; + method.IsPublic = true; + TypeDefinition.Methods.Add (method); + } + + [Test] + public void Compile_MethodCall () + { + Expression e = () => Console.WriteLine ("constant"); + var m = ExpressionAssemblyBuilder.Compile (e); + + AddMethod (m); + + var expected = new[]{ + "IL_0000: ldstr \"constant\"", + "IL_0000: call System.Void System.Console::WriteLine(System.String)", + "IL_0000: ret", + }; + var actual = m.Body.Instructions; + Assert.AreEqual (expected.Length, actual.Count); + for (int i = 0; i < expected.Length; ++i) { + Assert.AreEqual (expected [i], actual [i].ToString ()); + } + } + + [Test] + public void Compile_Condition_1 () + { + Expression> e = (a, b) => a == b; + var m = ExpressionAssemblyBuilder.Compile (e); + + AddMethod (m); + + var expected = new[]{ + "Instruction_0000: ldarg.0", + "Instruction_0001: ldarg.1", + "Instruction_0002: ceq", + "Instruction_0003: ret", + }; + var actual = m.Body.Instructions; + Assert.AreEqual (expected.Length, actual.Count); + for (int i = 0; i < expected.Length; ++i) { + Assert.AreEqual (expected [i], GetDescription (actual, i)); + } + } + + [Test] + public void Compile_Condition_2 () + { + Expression> e = (a, b) => a == b ? 1 : 2; + var m = ExpressionAssemblyBuilder.Compile (e); + + AddMethod (m); + + // Alas, branch targets d + var expected = new[]{ + "Instruction_0000: ldarg.0", + "Instruction_0001: ldarg.1", + "Instruction_0002: ceq", + "Instruction_0003: brfalse Instruction_0006", + "Instruction_0004: ldc.i4 1", + "Instruction_0005: br Instruction_0008", + "Instruction_0006: nop", + "Instruction_0007: ldc.i4 2", + "Instruction_0008: nop", + "Instruction_0009: ret", + }; + var actual = m.Body.Instructions; + Assert.AreEqual (expected.Length, actual.Count); + for (int i = 0; i < expected.Length; ++i) { + Assert.AreEqual (expected [i], GetDescription (actual, i)); + } + } + + [Test] + public void Compile_TryCatchFinally () + { + var exit = Expression.Label (typeof (int), "__exit"); + var tryBlock = Expression.Block (typeof (int), + E(() => Console.WriteLine ("try")).Body, + Expression.Return (target: exit, value: Expression.Constant (1), type: typeof (int)) + ); + var finallyBlock = E(() => Console.WriteLine ("finally")).Body; + var catchLog0 = E>(e => Console.WriteLine ("filtered")); + var catchFilt0 = Expression.Equal ( + Expression.Constant (null, typeof (Exception)), + Expression.Property (catchLog0.Parameters [0], "InnerException")); + var catchBlock0 = Expression.Block (typeof (int), + catchLog0.Body, + Expression.Return (target: exit, value: Expression.Constant (3), type: typeof (int)) + ); + var catchLog1 = E>(e => Console.WriteLine (e.ToString ())); + var catchBlock1 = Expression.Block (typeof (int), + catchLog1.Body, + Expression.Return (target: exit, value: Expression.Constant (4), type: typeof (int)) + ); + var block = new List { + Expression.TryCatchFinally ( + body: tryBlock, + @finally: finallyBlock, + handlers: new[]{ + Expression.Catch (catchLog0.Parameters[0], catchBlock0, catchFilt0), + Expression.Catch (catchLog1.Parameters[0], catchBlock1), + } + ), + Expression.Label (exit, Expression.Default (typeof (int))), + }; + var e = Expression.Lambda ( + delegateType: typeof (Func), + body: Expression.Block (variables: Array.Empty(), expressions: block), + name: nameof (Compile_TryCatchFinally), + tailCall: false, + parameters: Array.Empty() + ); + + Assert.AreEqual (1, ((Func) e.Compile ())()); + + var expectedCsharp = @"int Compile_TryCatchFinally() +{ + try + { + Console.WriteLine(""try""); + return 1; + } + catch (Exception e) if (null == e.InnerException) + { + Console.WriteLine(""filtered""); + return 3; + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return 4; + } + finally + { + Console.WriteLine(""finally""); + } +}"; + Console.WriteLine ($"# jonp: expression tree as C#:"); + Console.WriteLine (e.ToCSharpCode ()); + Assert.AreEqual (expectedCsharp, e.ToCSharpCode ()); + + var m = ExpressionAssemblyBuilder.Compile (e); + + AddMethod (m); + DumpInstructions (m); + + // Alas, branch targets d + var expected = new[]{ + // .try + "Instruction_0000: ldstr \"try\"", + "Instruction_0001: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0002: ldc.i4 1", + "Instruction_0003: stloc.0", + "Instruction_0004: leave Instruction_0023", + // } + // filter { + "Instruction_0005: isinst System.Exception", + "Instruction_0006: dup", + "Instruction_0007: brtrue.s Instruction_000b", + "Instruction_0008: pop", + "Instruction_0009: ldc.i4.0", + "Instruction_000a: br.s Instruction_0012", + "Instruction_000b: stloc.1", + "Instruction_000c: ldnull", + "Instruction_000d: ldloc.1", + "Instruction_000e: callvirt System.Exception System.Exception::get_InnerException()", + "Instruction_000f: ceq", + "Instruction_0010: ldc.i4.0", + "Instruction_0011: cgt.un", + "Instruction_0012: endfilter", + // } + // { // handler + "Instruction_0013: stloc.1", + "Instruction_0014: ldstr \"filtered\"", + "Instruction_0015: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0016: ldc.i4 3", + "Instruction_0017: stloc.0", + "Instruction_0018: leave Instruction_0023", + // } + // catch class System.Exception { + "Instruction_0019: stloc.2", + "Instruction_001a: ldloc.2", + "Instruction_001b: call System.String System.Object::ToString()", + "Instruction_001c: call System.Void System.Console::WriteLine(System.String)", + "Instruction_001d: ldc.i4 4", + "Instruction_001e: stloc.0", + "Instruction_001f: leave Instruction_0023", + // } + // finally { + "Instruction_0020: ldstr \"finally\"", + "Instruction_0021: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0022: endfinally", + // } + "Instruction_0023: nop", + "Instruction_0024: ldloc.0", + "Instruction_0025: ret", + }; + var actual = m.Body.Instructions; + Assert.AreEqual (expected.Length, actual.Count); + for (int i = 0; i < expected.Length; ++i) { + Assert.AreEqual (expected [i], GetDescription (actual, i)); + } + } + + static Expression E(Expression e) + where TDelegate : Delegate + { + return e; + } + + + static void DumpInstructions (MethodDefinition method) + { + var body = method.Body; + var instructions = body.Instructions; + if (body.HasExceptionHandlers) { + foreach (var h in method.Body.ExceptionHandlers) { + Console.Error.WriteLine ($"// Handler: {h.HandlerType}"); + Console.Error.WriteLine( $"// \t" + + $" CatchType=`{h.CatchType}`"); + Console.Error.WriteLine ($"// \t" + + $" TryStart=`{GetDescription (body.Instructions, body.Instructions.IndexOf (h.TryStart))}` TryEnd=`{GetDescription (body.Instructions, body.Instructions.IndexOf (h.TryEnd))}`"); + Console.Error.WriteLine ($"// \t" + + $" FilterStart=`{GetDescription (body.Instructions, body.Instructions.IndexOf (h.FilterStart))}`"); + Console.Error.WriteLine ($"// \t" + + $" HandlerStart=`{GetDescription (body.Instructions, body.Instructions.IndexOf (h.HandlerStart))}` HandlerEnd=`{GetDescription (body.Instructions, body.Instructions.IndexOf (h.HandlerEnd))}`"); + Console.Error.WriteLine($""); + } + } + int indent = 0; + for (int i = 0; i < instructions.Count; ++i) { + var instruction = instructions [i]; + DumpStartHandler (ref indent, body, instructions, i); + Console.Error.WriteLine ("{0}{1,-40}\t; {2}", + new string (' ', indent*2), + GetDescription (instructions, i), + instructions[i].ToString ()); + DumpEndHandler (ref indent, body, instructions, i); + } + } + + static void DumpStartHandler (ref int indent, MethodBody body, Mono.Collections.Generic.Collection instructions, int i) + { + var instruction = instructions [i]; + if (!body.HasExceptionHandlers) { + return; + } + if (body.ExceptionHandlers.Any (e => e.TryStart == instruction)) { + Console.Error.WriteLine ($"{new string (' ', indent*2)}.try {{"); + indent++; + return; + } + var f = body.ExceptionHandlers.FirstOrDefault (e => e.FilterStart == instruction); + if (f != null) { + Console.Error.WriteLine ($"{new string(' ', indent*2)}filter {{"); + indent++; + return; + + } + var h = body.ExceptionHandlers.FirstOrDefault (e => e.HandlerStart == instruction); + if (h != null) { + switch (h.HandlerType) { + case ExceptionHandlerType.Finally: + Console.Error.WriteLine ($"{new string (' ', indent*2)}finally {{"); + break; + case ExceptionHandlerType.Catch: + Console.Error.WriteLine ($"{new string (' ', indent*2)}catch class {h.CatchType.FullName} {{"); + break; + case ExceptionHandlerType.Filter: + Console.Error.WriteLine ($"{new string(' ', indent * 2)}{{ // handler"); + break; + case ExceptionHandlerType.Fault: + default: + Console.Error.WriteLine ($"{new string (' ', indent*2)}{h.HandlerType} {{"); + break; + } + indent++; + return; + } + } + + static void DumpEndHandler (ref int indent, MethodBody body, Mono.Collections.Generic.Collection instructions, int i) + { + if (!body.HasExceptionHandlers) { + return; + } + if ((i + 1) >= instructions.Count) { + // End of instruction stream; clean up indentatino + if (indent == 0) + return; + indent--; + Console.Error.WriteLine ($"{new string (' ', indent)}}}"); + return; + } + // Handler range is from first label ***prior to*** second (emphasis @jonpryor) + // Thus, look at *next* instruction. + var instruction = instructions[i+1]; + if (body.ExceptionHandlers.Any (e => e.TryStart == instruction || e.FilterStart == instruction || e.HandlerStart == instruction || + e.TryEnd == instruction || e.HandlerEnd== instruction)) { + indent--; + Console.Error.WriteLine ($"{new string (' ', indent)}}}"); + } + } + + // Cribbed with changes from `Instruction.ToString()`: + // https://github.com/dotnet/cecil/blob/e069cd8d25d5b61b0e28fe65e75959c20af7aa80/Mono.Cecil.Cil/Instruction.cs#L95-L134 + // + // Don't want to use `Instruction.ToString()` as `Instruction.Offset` isn't updated until after + // `AssemblyDefinition.Write()`, and checking for `brfalse IL_0000` is not helpful. + static string GetDescription (IList instructions, int index) + { + if (index < 0) { + return ""; + } + var instruction = instructions [index]; + var description = new StringBuilder (); + + AppendLabel (index) + .Append (": ") + .Append (instruction.OpCode.Name); + + if (instruction.Operand == null) { + return description.ToString (); + } + + description.Append (" "); + + switch (instruction.OpCode.OperandType) { + case OperandType.ShortInlineBrTarget: + case OperandType.InlineBrTarget: + AppendLabel (instructions.IndexOf ((Instruction) instruction.Operand)); + break; + case OperandType.InlineSwitch: + var labels = (Instruction []) instruction.Operand; + for (int i = 0; i < labels.Length; i++) { + if (i > 0) + description.Append (','); + + AppendLabel (instructions.IndexOf (labels [i])); + } + break; + case OperandType.InlineString: + description.Append ('\"'); + description.Append (instruction.Operand); + description.Append ('\"'); + break; + default: + description.Append (instruction.Operand); + break; + } + + return description.ToString (); + + StringBuilder AppendLabel (int i) + { + return description.Append ("Instruction_") + .AppendFormat ("{0:x4}", i); + } + } +} diff --git a/tests/Java.Interop.Tools.Expressions-Tests/Usings.cs b/tests/Java.Interop.Tools.Expressions-Tests/Usings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/tests/Java.Interop.Tools.Expressions-Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index d4020fd51..1e66a4040 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -1,17 +1,21 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.Loader; using System.Text.RegularExpressions; using Java.Interop; using Mono.Cecil; +using Mono.Cecil.Cil; using Mono.Options; using Mono.Collections.Generic; using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.Expressions; #if _DUMP_REGISTER_NATIVE_MEMBERS using Mono.Linq.Expressions; @@ -23,9 +27,8 @@ class App : MarshalByRefObject { internal const string Name = "jnimarshalmethod-gen"; - static DirectoryAssemblyResolver resolver = new DirectoryAssemblyResolver (logger: (l, v) => { Console.WriteLine (v); }, loadDebugSymbols: true, loadReaderParameters: new ReaderParameters () { ReadSymbols = true, InMemory = true }); + static DirectoryAssemblyResolver resolver; static readonly TypeDefinitionCache cache = new TypeDefinitionCache (); - static Dictionary definedTypes = new Dictionary (); static Dictionary typeMap = new Dictionary (); static List references = new List (); static public bool Debug; @@ -35,8 +38,23 @@ class App : MarshalByRefObject static List typeNameRegexes = new List (); static string jvmDllPath; List FilesToDelete = new List (); + // AssemblyLoadContext loadContext; static string outDirectory; + static App() + { + var r = new ReaderParameters { + ReadSymbols = true, + InMemory = true, + ReflectionImporterProvider = new RemappingReflectionImporterProvider (), + }; + resolver = new DirectoryAssemblyResolver ( + logger: (l, v) => { Console.WriteLine(v); }, + loadDebugSymbols: true, + loadReaderParameters: r + ); + } + public static int Main (string [] args) { var app = new App (); @@ -156,21 +174,48 @@ void ProcessAssemblies (List assemblies) InMemory = true, ReadSymbols = true, ReadWrite = string.IsNullOrEmpty (outDirectory), + ReflectionImporterProvider = new RemappingReflectionImporterProvider (), }; var readerParametersNoSymbols = new ReaderParameters { AssemblyResolver = resolver, InMemory = true, ReadSymbols = false, ReadWrite = string.IsNullOrEmpty (outDirectory), + ReflectionImporterProvider = new RemappingReflectionImporterProvider (), + }; + + foreach (var r in references) { + resolver.SearchDirectories.Add (Path.GetDirectoryName (r)); + } + foreach (var assembly in assemblies) { + resolver.SearchDirectories.Add (Path.GetDirectoryName (assembly)); + } + + // loadContext = CreateLoadContext (); + AppDomain.CurrentDomain.AssemblyResolve += (o, e) => { + Console.WriteLine ($"# jonp: resolving: {e.Name}"); + foreach (var d in resolver.SearchDirectories) { + var a = Path.Combine (d, e.Name); + var f = a + ".dll"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + f = a + ".exe"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + } + return null; }; foreach (var r in references) { try { + // loadContext.LoadFromAssemblyPath (Path.GetFullPath (r)); Assembly.LoadFile (Path.GetFullPath (r)); - } catch (Exception) { + } catch (Exception e) { + Console.WriteLine (e); ErrorAndExit (Message.ErrorUnableToPreloadReference, r); } - resolver.SearchDirectories.Add (Path.GetDirectoryName (r)); } foreach (var assembly in assemblies) { @@ -178,7 +223,6 @@ void ProcessAssemblies (List assemblies) ErrorAndExit (Message.ErrorPathDoesNotExist, assembly); } - resolver.SearchDirectories.Add (Path.GetDirectoryName (assembly)); AssemblyDefinition ad; try { ad = AssemblyDefinition.ReadAssembly (assembly, readerParameters); @@ -197,7 +241,6 @@ void ProcessAssemblies (List assemblies) foreach (var assembly in assemblies) { try { CreateMarshalMethodAssembly (assembly); - definedTypes.Clear (); } catch (Exception e) { ErrorAndExit (Message.ErrorUnableToProcessAssembly, assembly, Environment.NewLine, e.Message, e); } @@ -217,27 +260,36 @@ void CreateJavaVM (string jvmDllPath) } } - static JniRuntime.JniMarshalMemberBuilder CreateExportedMemberBuilder () + AssemblyLoadContext CreateLoadContext () { - return JniEnvironment.Runtime.MarshalMemberBuilder; + var c = new AssemblyLoadContext ("jnimarshalmethod-gen", isCollectible: true); + c.Resolving += (context, name) => { + Console.WriteLine ($"# jonp: trying to load assembly: {name}"); + if (name.Name == "Java.Interop") { + return typeof (IJavaPeerable).Assembly; + } + if (name.Name == "Java.Interop.Export") { + return typeof (JavaCallableAttribute).Assembly; + } + foreach (var d in resolver.SearchDirectories) { + var a = Path.Combine (d, name.Name); + var f = a + ".dll"; + if (File.Exists (f)) { + return context.LoadFromAssemblyPath (Path.GetFullPath (f)); + } + f = a + ".exe"; + if (File.Exists (f)) { + return context.LoadFromAssemblyPath (Path.GetFullPath (f)); + } + } + return null; + }; + return c; } - static TypeBuilder GetTypeBuilder (ModuleBuilder mb, Type type) + static JniRuntime.JniMarshalMemberBuilder CreateExportedMemberBuilder () { - if (definedTypes.ContainsKey (type.FullName)) - return definedTypes [type.FullName]; - - if (type.IsNested) { - var outer = GetTypeBuilder (mb, type.DeclaringType); - var nested = outer.DefineNestedType (type.Name, System.Reflection.TypeAttributes.NestedPublic); - definedTypes [type.FullName] = nested; - return nested; - } - - var tb = mb.DefineType (type.FullName, System.Reflection.TypeAttributes.Public); - definedTypes [type.FullName] = tb; - - return tb; + return JniEnvironment.Runtime.MarshalMemberBuilder; } class MethodsComparer : IComparer @@ -278,7 +330,6 @@ public int Compare (MethodInfo a, MethodInfo b) void CreateMarshalMethodAssembly (string path) { - var assembly = Assembly.Load (File.ReadAllBytes (Path.GetFullPath (path))); var baseName = Path.GetFileNameWithoutExtension (path); var assemblyName = new AssemblyName (baseName + "-JniMarshalMethods"); var fileName = assemblyName.Name + ".dll"; @@ -289,17 +340,15 @@ void CreateMarshalMethodAssembly (string path) if (Verbose) ColorWriteLine ($"Preparing marshal method assembly '{assemblyName}'", ConsoleColor.Cyan); - var da = AppDomain.CurrentDomain.DefineDynamicAssembly ( - assemblyName, - AssemblyBuilderAccess.Save, - destDir); - - var dm = da.DefineDynamicModule ("", fileName); - var ad = resolver.GetAssembly (path); + var assemblyBuilder = new ExpressionAssemblyBuilder (ad); + PrepareTypeMap (ad.MainModule); +// var assembly = loadContext.LoadFromStream (File.OpenRead (path)); + var assembly = Assembly.LoadFrom (Path.GetFullPath (path)); + Type[] types = null; try { types = assembly.GetTypes (); @@ -307,6 +356,9 @@ void CreateMarshalMethodAssembly (string path) types = e.Types; foreach (var le in e.LoaderExceptions) Warning (Message.WarningTypeLoadException, Environment.NewLine, le); + if (Verbose) { + ColorMessage ($"Exception: {e.ToString ()}", ConsoleColor.Red, Console.Error); + } } foreach (var systemType in types) { @@ -349,26 +401,49 @@ void CreateMarshalMethodAssembly (string path) if (Verbose) ColorWriteLine ($"Processing {type} type", ConsoleColor.Yellow); - var registrationElements = new List (); + var registrations = new List (); var targetType = Expression.Variable (typeof(Type), "targetType"); - TypeBuilder dt = null; var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; var methods = type.GetMethods (flags); Array.Sort (methods, new MethodsComparer (type, td)); + Console.WriteLine ($"# jonp: methods: {string.Join (Environment.NewLine, methods)}"); addedMethods.Clear (); + var mmTypeDef = new TypeDefinition ( + @namespace: null, + name: TypeMover.NestedName, + attributes: Mono.Cecil.TypeAttributes.NestedPrivate + ); + mmTypeDef.BaseType = assemblyBuilder.DeclaringAssemblyDefinition.MainModule.TypeSystem.Object; + foreach (var method in methods) { // TODO: Constructors var export = method.GetCustomAttribute (); + Console.WriteLine ($"# jonp: checking method {method}; export? {export != null}"); + #if false + if (method.Name == "InstanceAction") { + foreach (var x in method.GetCustomAttributes (inherit:false)) { + if (x.GetType ().Name == "JavaCallableAttribute") { + Console.WriteLine ($"# jonp: {x} == {typeof (JavaCallableAttribute)}? {x.GetType () == typeof (JavaCallableAttribute)}"); + } + } + } + #endif + var exportObj = method.GetCustomAttributes (inherit:false).SingleOrDefault (a => a.GetType ().Name == "JavaCallableAttribute"); string signature = null; string name = null; string methodName = method.Name; - if (export == null) { + if (exportObj != null) { + dynamic e = exportObj; + name = e.Name; + signature = e.Signature; + } + else { if (method.IsGenericMethod || method.ContainsGenericParameters || method.IsGenericMethodDefinition || method.ReturnType.IsGenericType) continue; @@ -388,59 +463,62 @@ void CreateMarshalMethodAssembly (string path) continue; } - if (dt == null) - dt = GetTypeBuilder (dm, type); - - if (addedMethods.Contains (methodName)) + if (addedMethods.Contains (methodName)) { + Console.WriteLine ($"# jonp: method already added (?!)"); continue; + } if (Verbose) { Console.Write ("Adding marshal method for "); ColorWriteLine ($"{method}", ConsoleColor.Green ); } - var mb = dt.DefineMethod ( - methodName, - System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static); - var lambda = builder.CreateMarshalToManagedExpression (method); - lambda.CompileToMethod (mb); +#if _DUMP_REGISTER_NATIVE_MEMBERS + Console.WriteLine ($"## Dumping contents of marshal method for `{td.FullName}::{method.Name}({string.Join (", ", method.GetParameters ().Select (p => p.ParameterType))})`:"); + Console.WriteLine (lambda.ToCSharpCode ()); +#endif // _DUMP_REGISTER_NATIVE_MEMBERS + var mmDef = assemblyBuilder.Compile (lambda); + mmDef.Name = export?.Name ?? ("n_TODO" + lambda.GetHashCode ()); + mmTypeDef.Methods.Add (mmDef); + Console.WriteLine ($"# jonp: marshal method body instructions count: {mmDef.Body.Instructions.Count}"); if (export != null) { name = export.Name; signature = export.Signature; } - if (signature == null) + if (signature == null) { signature = builder.GetJniMethodSignature (method); + } - registrationElements.Add (CreateRegistration (name, signature, lambda, targetType, methodName)); + registrations.Add (new ExpressionMethodRegistration (name, signature, mmDef)); addedMethods.Add (methodName); } - if (dt != null) - AddRegisterNativeMembers (dt, targetType, registrationElements); + if (registrations.Count > 0) { + var m = assemblyBuilder.CreateRegistrationMethod (registrations); + mmTypeDef.Methods.Add (m); + td.NestedTypes.Add (mmTypeDef); + } } - foreach (var tb in definedTypes) - tb.Value.CreateType (); - - da.Save (fileName); - if (Verbose) ColorWriteLine ($"Marshal method assembly '{assemblyName}' created", ConsoleColor.Cyan); resolver.SearchDirectories.Add (destDir); - var dstAssembly = resolver.GetAssembly (fileName); + // var dstAssembly = resolver.GetAssembly (fileName); if (!string.IsNullOrEmpty (outDirectory)) path = Path.Combine (outDirectory, Path.GetFileName (path)); - var mover = new TypeMover (dstAssembly, ad, path, definedTypes, resolver, cache); - mover.Move (); + assemblyBuilder.Write (path); + + // var mover = new TypeMover (dstAssembly, ad, path, definedTypes, resolver, cache); + // mover.Move (); - if (!keepTemporary) - FilesToDelete.Add (dstAssembly.MainModule.FileName); + // if (!keepTemporary) + // FilesToDelete.Add (dstAssembly.MainModule.FileName); } static readonly MethodInfo Delegate_CreateDelegate = typeof (Delegate).GetMethod ("CreateDelegate", new[] { @@ -482,32 +560,6 @@ static Expression CreateRegistration (string method, string signature, LambdaExp d); } - static void AddRegisterNativeMembers (TypeBuilder dt, ParameterExpression targetType, List registrationElements) - { - if (Verbose) { - Console.Write ("Adding registration method for "); - ColorWriteLine ($"{dt.FullName}", ConsoleColor.Green); - } - - var args = Expression.Parameter (typeof (JniNativeMethodRegistrationArguments), "args"); - var body = Expression.Block ( - new[]{targetType}, - Expression.Assign (targetType, Expression.Call (Type_GetType, Expression.Constant (dt.FullName))), - Expression.Call (args, JniNativeMethodRegistrationArguments_AddRegistrations, Expression.NewArrayInit (typeof (JniNativeMethodRegistration), registrationElements.ToArray ()))); - - var lambda = Expression.Lambda> (body, new[]{ args }); - - var rb = dt.DefineMethod ("__RegisterNativeMembers", - System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static); - rb.SetParameters (typeof (JniNativeMethodRegistrationArguments)); - rb.SetCustomAttribute (new CustomAttributeBuilder (typeof (JniAddNativeMethodRegistrationAttribute).GetConstructor (Type.EmptyTypes), new object[0])); -#if _DUMP_REGISTER_NATIVE_MEMBERS - Console.WriteLine ($"## Dumping contents of `{dt.FullName}::__RegisterNativeMembers`: "); - Console.WriteLine (lambda.ToCSharpCode ()); -#endif // _DUMP_REGISTER_NATIVE_MEMBERS - lambda.CompileToMethod (rb); - } - static void ColorMessage (string message, ConsoleColor color, TextWriter writer, bool writeLine = true) { Console.ForegroundColor = color; diff --git a/tools/jnimarshalmethod-gen/Xamarin.Android.Tools.JniMarshalMethodGenerator.csproj b/tools/jnimarshalmethod-gen/Xamarin.Android.Tools.JniMarshalMethodGenerator.csproj index c540acb30..746c6d749 100644 --- a/tools/jnimarshalmethod-gen/Xamarin.Android.Tools.JniMarshalMethodGenerator.csproj +++ b/tools/jnimarshalmethod-gen/Xamarin.Android.Tools.JniMarshalMethodGenerator.csproj @@ -1,7 +1,7 @@  - net472 + $(DotNetTargetFramework) Exe 8.0 jnimarshalmethod-gen @@ -13,6 +13,7 @@ $(UtilityOutputFullPath) + <_DumpRegisterNativeMembers>True @@ -35,6 +36,7 @@ +