From 77800dda83c2db4d90b501c00069abc9880caaeb Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 27 Feb 2023 21:18:03 -0500 Subject: [PATCH] [Java.Interop.Tools.Expressions] Add Java.Interop.Tools.Expressions (#1046) 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 (xamarin/java.interop#616)? 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! ~~ Usage ~~ % dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll \ bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll \ -v -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` processed. * `-v -v` is *really* verbose output * `--keeptemp` is keep temporary files, in this case `_x/Java.Interop.Export-Tests.dll.cecil`. * `--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). By default the directories containing input assemblies and the directory containing `System.Private.CoreLib.dll` are part of the default `-L` list. When running in-tree, e.g. AzDO pipeline execution, when `--jvm PATH` isn't specified, `jnimarshalmethod-gen` will attempt to read the path in `bin/Build*/JdkInfo.props` a'la `TestJVM` (002dea4a). This allows an in-place update in `core-tests.yaml` which does: dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll \ bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll \ -v -v --keeptemp -o bin/TestDebug-net7.0 ~~ Using `jnimarshalmethod-gen` output ~~ What does `jnimarshalmethod-gen` *do*? % ikdasm bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll.orig > beg.il % ikdasm bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll > end.il % git diff --no-index beg.il end.il # https://gist.github.com/jonpryor/b8233444f2e51043732bea922f6afc81 is a ~1KB 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_L (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. What can we do with this `jnimarshalmethod-gen` output? Use it! First, make sure the tests work: # do this *before* running above `dotnet jnimarshalmethod-gen.dll` command… % dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll … Passed! - Failed: 0, Passed: 17, Skipped: 0, Total: 17, Duration: 103 ms - Java.Interop.Export-Tests.dll (net7.0) Then after running the above `dotnet jnimarshalmethod-gen.dll` command, re-run the tests: % dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll … Total tests: 17 Passed: 17 `core-tests.yaml` has been updated to do this. ~~ One-Off Tests ~~ 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 # which currently fails :-( Re-enable most of `Java.Interop.Export-Tests.dll` for .NET 7; see 41ba3485, which disabled those tests. To verify the generated IL, use the [dotnet-ilverify][0] tool: dotnet tool install --global dotnet-ilverify Usage of which is "weird": $HOME/.dotnet/tools/ilverify bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll \ --tokens --system-module System.Private.CoreLib \ -r 'bin/TestDebug-net7.0/*.dll' \ -r '/usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0/*.dll' All Classes and Methods in /Volumes/Xamarin-Work/src/xamarin/Java.Interop/_x/Java.Interop.Export-Tests.dll Verified. # no errors! where: * `--tokens`: Include metadata tokens in error messages. * `--system-module NAME`: set the "System module name". Defaults to `mscorlib`, which is wrong for .NET 5+, so this must be set to `System.Private.CoreLib` (no `.dll` suffix!). * `-r FILE-GLOB`: Where to resolve assembly references for the input assembly. Fortunately file globs are supported… ~~ Removing `System.Private.CoreLib` ~~ `System.Private.CoreLib.dll` is *private*; it's not part of the public assembly surface area, so you can't use `csc -r:System.Private.CoreLib …` and expect it to work. This makes things interesting because *at runtime* everything "important" is in `System.Private.CoreLib.dll`, like `System.Object`. Specifically, if we do the "obvious" thing and do: newTypeDefinition.BaseType = assemblyDefinition.MainModule .ImportReference (typeof (object)); you're gonna have a bad type, because the resulting IL for `newTypeDefinition` will have a base class of `[System.Private.CoreLib]System.Object`, which isn't usable. Fix this by: 1. Writing the assembly to a `Stream`. 2. Reading the `Stream` from (1) 3. Fixing all member references and assembly references so that `System.Private.CoreLib` is not referenced. If `jnimarshalmethod-gen.dll --keeptemp` is used, then a `.cecil` file is created with the contents of (1). Additionally, and unexpectedly -- [jbevain/cecil#895][1] -- Mono.Cecil adds a reference to the assembly being modified. Remove the declaring assembly from `AssemblyReferences`. [0]: https://www.nuget.org/packages/dotnet-ilverify [1]: https://github.com/jbevain/cecil/issues/895 --- Directory.Build.props | 1 - Java.Interop.sln | 14 + Makefile | 33 +- TargetFrameworkDependentValues.props | 2 + .../automation/templates/core-tests.yaml | 20 +- src/Java.Base-ref.cs | 2 +- .../Java.Interop.Export.csproj | 4 +- .../Java.Interop/MarshalMemberBuilder.cs | 24 +- .../Java.Interop.Tools.Expressions.csproj | 27 + .../CecilCompilerExpressionVisitor.cs | 826 ++++++++++++++++++ .../ExpressionAssemblyBuilder.cs | 452 ++++++++++ .../ExpressionMethodRegistration.cs | 9 + .../Java.Interop/MarshalMemberBuilderTest.cs | 22 +- ...ava.Interop.Tools.Expressions-Tests.csproj | 36 + .../ExpressionAssemblyBuilderTests.cs | 444 ++++++++++ .../Usings.cs | 1 + tools/jnimarshalmethod-gen/App.cs | 337 +++++-- ...oid.Tools.JniMarshalMethodGenerator.csproj | 4 +- 18 files changed, 2134 insertions(+), 124 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 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/Directory.Build.props b/Directory.Build.props index 637ff10a6..95d93ffc2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -72,7 +72,6 @@ <_JavacSourceOptions>-source $(JavacSourceVersion) -target $(JavacTargetVersion) $(_BootClassPath) - <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll $([System.IO.Path]::GetFullPath ('$(XamarinAndroidToolsDirectory)')) 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/Makefile b/Makefile index 2bbf73873..8f31778fd 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,8 @@ PREPARE_EXTERNAL_FILES = \ DEPENDENCIES = \ bin/Test$(CONFIGURATION)/libNativeTiming$(NATIVE_EXT) +NET_SUFFIX = -net7.0 + TESTS = \ bin/Test$(CONFIGURATION)/Java.Interop-Tests.dll \ bin/Test$(CONFIGURATION)/Java.Interop.Dynamic-Tests.dll \ @@ -33,7 +35,7 @@ TESTS = \ bin/Test$(CONFIGURATION)/Xamarin.SourceWriter-Tests.dll NET_TESTS = \ - bin/Test$(CONFIGURATION)-net7.0/Java.Base-Tests.dll + bin/Test$(CONFIGURATION)$(NET_SUFFIX)/Java.Base-Tests.dll PTESTS = \ bin/Test$(CONFIGURATION)/Java.Interop-PerformanceTests.dll @@ -43,6 +45,10 @@ ATESTS = \ all: $(DEPENDENCIES) $(TESTS) +bin/ilverify: + -mkdir bin + dotnet tool install --tool-path bin dotnet-ilverify + run-all-tests: r=0; \ $(MAKE) run-tests || r=1 ; \ @@ -127,7 +133,7 @@ run-tests: $(TESTS) bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB) $(foreach t,$(TESTS), $(call RUN_TEST,$(t),1)) \ exit $$r; -run-net-tests: $(NET_TESTS) bin/Test$(CONFIGURATION)-net7.0/$(JAVA_INTEROP_LIB) +run-net-tests: $(NET_TESTS) bin/Test$(CONFIGURATION)$(NET_SUFFIX)/$(JAVA_INTEROP_LIB) r=0; \ $(foreach t,$(NET_TESTS), dotnet test $(t) || r=1) \ exit $$r; @@ -150,15 +156,28 @@ $(JRE_DLL_CONFIG): src/Java.Runtime.Environment/Java.Runtime.Environment.csproj define run-jnimarshalmethod-gen MONO_TRACE_LISTENER=Console.Out \ - $(RUNTIME) bin/$(CONFIGURATION)/jnimarshalmethod-gen.exe -v --jvm "$(JI_JVM_PATH)" -L "$(JI_MONO_LIB_PATH)mono/4.5" -L "$(JI_MONO_LIB_PATH)mono/4.5/Facades" $(2) $(1) + dotnet bin/$(CONFIGURATION)$(NET_SUFFIX)/jnimarshalmethod-gen.dll $(2) $(1) endef -run-test-jnimarshal: bin/Test$(CONFIGURATION)/Java.Interop.Export-Tests.dll bin/Test$(CONFIGURATION)/$(JAVA_INTEROP_LIB) $(JRE_DLL_CONFIG) +# want: /usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.0 +# have: Microsoft.NETCore.App 7.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] +# use: shell pipeline! +SYSTEM_NET_ASSEMBLIES_PATH := $(shell dotnet --list-runtimes | grep ^Microsoft.NETCore.App | tail -1 | sed -E 's,^Microsoft.NETCore.App ([^ ]+) \[([^]]+)\]$$,\2/\1,g' ) + +run-test-jnimarshal: bin/Test$(CONFIGURATION)$(NET_SUFFIX)/Java.Interop.Export-Tests.dll bin/Test$(CONFIGURATION)$(NET_SUFFIX)/$(JAVA_INTEROP_LIB) bin/ilverify mkdir -p test-jni-output - $(call run-jnimarshalmethod-gen,"$<",-f -o test-jni-output --keeptemp) - (test -f test-jni-output/$(notdir $<) && test -f test-jni-output/Java.Interop.Export-Tests-JniMarshalMethods.dll) || { echo "jnimarshalmethod-gen did not create the expected assemblies in the test-jni-output directory"; exit 1; } + # Do we run w/o error? + $(call run-jnimarshalmethod-gen,"$<", -v -v -o test-jni-output --keeptemp) + (test -f test-jni-output/$(notdir $<) ) || { echo "jnimarshalmethod-gen did not create the expected assemblies in the test-jni-output directory"; exit 1; } + # Is output valid? + ikdasm test-jni-output/Java.Interop.Export-Tests.dll || { echo "output can not be processed by ikdasm"; exit 1; } + bin/ilverify test-jni-output/Java.Interop.Export-Tests.dll \ + --tokens --system-module System.Private.CoreLib -r '$(dir $<)/*.dll' \ + -r '$(SYSTEM_NET_ASSEMBLIES_PATH)/*.dll' || { echo "ilverify found issues"; exit 1; } + # replace "original" assembly $(call run-jnimarshalmethod-gen,"$<") - $(call RUN_TEST,$<) + # make sure tests still pass + dotnet test $< bin/Test$(CONFIGURATION)/generator.exe: bin/$(CONFIGURATION)/generator.exe cp $<* `dirname "$@"` diff --git a/TargetFrameworkDependentValues.props b/TargetFrameworkDependentValues.props index ace8156d6..263520c42 100644 --- a/TargetFrameworkDependentValues.props +++ b/TargetFrameworkDependentValues.props @@ -12,6 +12,7 @@ $(MSBuildThisFileDirectory)bin\Test$(Configuration)-$(TargetFramework.ToLowerInvariant())\ $(UtilityOutputFullPathCoreApps) $(ToolOutputFullPath) + <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPathCoreApps)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPathCoreApps)Xamarin.Android.Cecil.dll Major $(JINetToolVersion) $(JINetCoreLibVersion) @@ -23,6 +24,7 @@ $(MSBuildThisFileDirectory)bin\$(Configuration)\ $(MSBuildThisFileDirectory)bin\Test$(Configuration)\ $(ToolOutputFullPath) + <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll $(JIOldToolVersion) $(JIOldCoreLibVersion) diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index 5f56f1797..3ec4ee8f6 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -105,13 +105,31 @@ steps: - task: DotNetCoreCLI@2 displayName: 'Tests: Java.Interop.Export' - condition: eq('${{ parameters.runNativeTests }}', 'true') + condition: or(eq('${{ parameters.runNativeDotnetTests }}', 'true'), eq('${{ parameters.runNativeTests }}', 'true')) inputs: command: test testRunTitle: Java.Interop.Export (${{ parameters.platformName }}) arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop.Export-Tests.dll continueOnError: true +- task: DotNetCoreCLI@2 + displayName: 'jnimarshalmethod-gen Java.Interop.Export-Tests.dll' + condition: or(eq('${{ parameters.runNativeDotnetTests }}', 'true'), eq('${{ parameters.runNativeTests }}', 'true')) + inputs: + command: custom + custom: bin/$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/jnimarshalmethod-gen.dll + arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop.Export-Tests.dll -v -v --keeptemp -o bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix) + continueOnError: true + +- task: DotNetCoreCLI@2 + displayName: 'Tests: Java.Interop.Export w/ jnimarshalmethod-gen!' + condition: or(eq('${{ parameters.runNativeDotnetTests }}', 'true'), eq('${{ parameters.runNativeTests }}', 'true')) + inputs: + command: test + testRunTitle: Java.Interop.Export (jnimarshalmethod-gen + ${{ parameters.platformName }}) + arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop.Export-Tests.dll + continueOnError: true + - task: DotNetCoreCLI@2 displayName: 'Tests: Java.Interop-Performance-net472' condition: eq('${{ parameters.runNativeTests }}', 'true') 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..b4a97c94d 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 (StringComparer.Ordinal); + } + 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..19dc12a81 --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/CecilCompilerExpressionVisitor.cs @@ -0,0 +1,826 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Java.Interop.Tools.Expressions; + +class CecilCompilerExpressionVisitor : ExpressionVisitor +{ + public CecilCompilerExpressionVisitor (AssemblyDefinition declaringAssembly, MethodBody body, VariableDefinitions variables, Action logger) + { + this.assemblyDef = declaringAssembly; + this.body = body; + this.variables = variables; + il = body.GetILProcessor (); + Logger = logger; + } + + AssemblyDefinition assemblyDef; + MethodBody body; + ILProcessor il; + VariableDefinitions variables; + Dictionary> returnFixups = new (); + Action Logger; + + /// + /// 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitBinary: {node} [{node.NodeType}]"); + switch (node.NodeType) { + case ExpressionType.Assign: + var target = node.Left as ParameterExpression; + if (target == null) { + Logger (TraceLevel.Verbose, $"# jonp: don't know where to assign `{node.Left}`!"); + return base.VisitBinary (node); + } + Logger (TraceLevel.Verbose, $"# jonp: target={target}; target.Type={target.Type}; requires-&? {InstanceInvokeRequiresAddress (target.Type)}"); + if (InstanceInvokeRequiresAddress (target.Type) && node.Right is NewExpression n) { + variables [target].LoadAddress (il); + Visit (node.Right); + } else { + Visit (node.Right); + variables [target].Store (il); + } + break; + case ExpressionType.Equal: + Visit (node.Left); + Visit (node.Right); + il.Emit (OpCodes.Ceq); + break; + default: + Logger (TraceLevel.Verbose, $"# jonp: don't know how to emit binary expr {node.NodeType}!"); + base.VisitBinary (node); + break; + } + return node; + } + + static bool InstanceInvokeRequiresAddress (Type type) => type.IsValueType && !type.IsPrimitive; + + /// + /// 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); + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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)) { + Logger (TraceLevel.Verbose, $"# jonp: TODO load type {node.Value}"); + break; + } else if (node.Value == null) { + Logger (TraceLevel.Verbose, $"# jonp: TODO ldnull {node.Value}"); + il.Emit (OpCodes.Ldnull); + break; + } + goto default; + default: + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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 = GetFixupsForLabelTarget (node.Target); + fixups.Add (il.Body.Instructions.Last ()); + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitLabelTarget: {node}"); + if (node != null) { + il.Emit (OpCodes.Nop); + GetFixupsForLabelTarget (node).Add (il.Body.Instructions.Last ()); + } + return base.VisitLabelTarget (node); + } + + List GetFixupsForLabelTarget (LabelTarget target) + { + if (!returnFixups.TryGetValue (target, out List? 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitLabel: {node}"); + var target = il.Body.Instructions.Last (); + if (returnFixups.TryGetValue (node.Target, out List? fixups)) { + foreach (var replace in fixups) { + Logger (TraceLevel.Verbose, $"# jonp: VisitLabel: replacing instruction `{replace}` w/ `leave {target}"); + Debug.Assert (replace.OpCode == OpCodes.Ret || replace.OpCode == OpCodes.Nop); + 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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 (GetCallOpCode (getter!), 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMethodCall: {node}; node.Object={node.Object}"); + // We need to special-case `node.Object` handling + // https://github.com/dotnet/runtime/blob/edd23fcb1b350cb1a53fa409200da55e9c33e99e/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionVisitor.cs#L403-L413 + + if (node.Object is ParameterExpression target) { + if (InstanceInvokeRequiresAddress (target.Type)) { + variables [target].LoadAddress (il); + } else { + variables [target].Load (il); + } + } else { + Visit (node.Object); + } + foreach (var a in node.Arguments) { + Visit (a); + } + il.Emit (GetCallOpCode (node.Method), assemblyDef.MainModule.ImportReference (node.Method)); + + return node; + } + + OpCode GetCallOpCode (global::System.Reflection.MethodBase method) + { + if (method.IsStatic || (method.DeclaringType?.IsValueType ?? false)) + return OpCodes.Call; + return OpCodes.Callvirt; + } + + void EmitConsoleWriteLine (ILProcessor il, string message) + { + Action cwl = Console.WriteLine; + il.Emit (OpCodes.Ldstr, message); + il.Emit (OpCodes.Call, assemblyDef.MainModule.ImportReference (cwl.Method)); + } + + /// + /// 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitNew: {node}"); + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitParameter: {(node.Type.IsByRef ? "&" : "")}{node}"); + + if (node.Type.IsByRef) { + variables [node].LoadAddress (il); + } else { + 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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. + + Logger (TraceLevel.Verbose, $"# 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) { + il.Emit (OpCodes.Castclass, assemblyDef.MainModule.ImportReference (node.Variable.Type)); + variables [node.Variable!].Store (il); + } else { + il.Emit (OpCodes.Pop); + } + + Visit (node.Body); + EmitLeave (); + + 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) + { + Logger (TraceLevel.Verbose, $"# 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 ()); + } + } + + /// + /// 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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) + { + Logger (TraceLevel.Verbose, $"# 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..d136f86f3 --- /dev/null +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs @@ -0,0 +1,452 @@ +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; +using static System.Formats.Asn1.AsnWriter; + +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 bool KeepTemporaryFiles {get; set;} + + public MethodDefinition Compile (LambdaExpression expression) + { + var mmDef = CreateMethodDefinition (DeclaringAssemblyDefinition, expression); + var decls = new VariableDefinitions (DeclaringAssemblyDefinition, mmDef, expression, Logger); + var mmBody = mmDef.Body; + var il = mmBody.GetILProcessor (); + var v = new CecilCompilerExpressionVisitor (DeclaringAssemblyDefinition, mmBody, decls, Logger); + v.Visit (expression); + + if (expression.ReturnType != null && expression.ReturnType != typeof (void) && decls.ReturnValue == null) { + Logger (TraceLevel.Error, $"# jonp: validation error: expression has a return type but we didn't find a return value! expression={expression}"); + } + + 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) + ) { + Body = { + InitLocals = true, + }, + }; + 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 + ) { + Body = { + InitLocals = true, + }, + }; + + 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 (); + EmitConsoleWriteLine (il, $"# jonp: called __RegisterNativeMembers w/ {methods.Count} methods to register."); + il.Emit (OpCodes.Ldc_I4, methods.Count); + il.Emit (OpCodes.Newarr, DeclaringAssemblyDefinition.MainModule.ImportReference (arrayType.GetElementType ())); + // 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.Dup); // 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.GetElementType ()); // args [i] = new JniNativeMethodRegistration (…) + } + + il.Emit (OpCodes.Stloc_0); + + Action> addRegistrations = new JniNativeMethodRegistrationArguments ().AddRegistrations; + il.Emit (OpCodes.Ldarga_S, args); + il.Emit (OpCodes.Ldloc_0); + il.Emit (OpCodes.Call, DeclaringAssemblyDefinition.MainModule.ImportReference (addRegistrations.Method)); + il.Emit (OpCodes.Ret); + + + return registrations; + } + + void EmitConsoleWriteLine (ILProcessor il, string message) + { + Action cwl = Console.WriteLine; + il.Emit (OpCodes.Ldstr, message); + il.Emit (OpCodes.Call, DeclaringAssemblyDefinition.MainModule.ImportReference (cwl.Method)); + } + + // 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 = GetMarshalMethodDelegateName (method.Parameters, method.ReturnType); + + var delegateDef = DeclaringAssemblyDefinition.MainModule.GetType (delegateName.ToString ()); + if (delegateDef == null) { + delegateDef = CreateMarshalMethodDelegateType (delegateName, method.Parameters, method.ReturnType); + DeclaringAssemblyDefinition.MainModule.Types.Add (delegateDef); + } + return delegateDef.Methods.First (m => m.Name == ".ctor"); + } + + string GetMarshalMethodDelegateName (IList parameters, TypeReference returnType) + { + // 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 < parameters.Count; i++) { + delegateName.Append (GetJniMarshalDelegateParameterIdentifier (parameters [i].ParameterType)); + } + delegateName.Append ("_"); + delegateName.Append (GetJniMarshalDelegateParameterIdentifier (returnType)); + + return delegateName.ToString (); + } + + 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 TypeDefinition CreateMarshalMethodDelegateType (string delegateName, IList parameters, TypeReference returnType) + { + var delegateDef = new TypeDefinition ( + @namespace: "", + name: delegateName, + 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: returnType + ); + invoke.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + foreach (var p in parameters) { + invoke.Parameters.Add (new ParameterDefinition (p.Name, p.Attributes, p.ParameterType)); + } + delegateDef.Methods.Add (invoke); + + return delegateDef; + } + + + public void Write (string path) + { + Logger (TraceLevel.Verbose, $"# jonp: ExpressionAssemblyBuilder.Write to path={path}"); + var module = DeclaringAssemblyDefinition.MainModule; + + var c = new MemoryStream (); + DeclaringAssemblyDefinition.Write (c); + c.Position = 0; + + if (KeepTemporaryFiles) { + using var intermediate = File.Create (path + ".cecil"); + c.WriteTo (intermediate); + c.Position = 0; + } + + Logger (TraceLevel.Verbose, $"# jonp: ---"); + + var rp = new ReaderParameters { + InMemory = true, + ReadSymbols = false, + ReadWrite = false, + ReadingMode = ReadingMode.Immediate, + }; + var newAsm = AssemblyDefinition.ReadAssembly (c, rp); + module = newAsm.MainModule; + var systemRuntimeRef = module.AssemblyReferences.FirstOrDefault (r => r.Name == "System.Runtime"); + var privateCorelibRef = module.AssemblyReferences.FirstOrDefault (r => r.Name == "System.Private.CoreLib"); + + if (systemRuntimeRef == null && privateCorelibRef != null) { + systemRuntimeRef = GetSystemRuntimeReference (); + module.AssemblyReferences.Add (systemRuntimeRef); + } + + var selfRef = module.AssemblyReferences.FirstOrDefault (r => r.Name == newAsm.Name.Name); + foreach (var member in module.GetMemberReferences ()) { + Logger (TraceLevel.Verbose, $"# jonp: looking at ref for member: [{member.DeclaringType.Scope?.Name}]{member}"); + if (member.DeclaringType.Scope == privateCorelibRef) { + Logger (TraceLevel.Verbose, $"# jonp: Fixing scope ref for member: {member}"); + member.DeclaringType.Scope = systemRuntimeRef; + continue; + } + if (member.DeclaringType.Scope == selfRef) { + Logger (TraceLevel.Verbose, $"# jonp: Fixing scope self ref for member: {member}"); + member.DeclaringType.Scope = null; + continue; + } + } + foreach (var type in module.GetTypeReferences ()) { + Logger (TraceLevel.Verbose, $"# jonp: looking at ref for type: [{type.Scope}]{type}"); + if (type.Scope == privateCorelibRef) { + Logger (TraceLevel.Verbose, $"# jonp: Fixing scope ref for type: {type}"); + type.Scope = systemRuntimeRef; + continue; + } + if (type.Scope == selfRef) { + Logger (TraceLevel.Verbose, $"# jonp: Fixing scope self ref for type: {type}"); + type.Scope = null; + continue; + } + } + module.AssemblyReferences.Remove (privateCorelibRef); + if (selfRef != null) { + module.AssemblyReferences.Remove (selfRef); + } + newAsm.Write (path); + } + + static AssemblyNameReference GetSystemRuntimeReference () + { + var privateCorelibDir = Path.GetDirectoryName (typeof (object).Assembly.Location) ?? + throw new NotSupportedException ("Cannot find directory of `System.Private.CoreLib.dll`!"); + var systemRuntimePath = Path.Combine (privateCorelibDir, "System.Runtime.dll"); + if (!File.Exists (systemRuntimePath)) { + throw new NotSupportedException ($"Could not find `System.Runtime.dll`; looked at `{systemRuntimePath}`."); + } + var rp = new ReaderParameters { + InMemory = false, + ReadSymbols = false, + ReadWrite = false, + ReadingMode = ReadingMode.Deferred, + }; + using var systemRuntime = AssemblyDefinition.ReadAssembly (systemRuntimePath, rp); + var nameDef = systemRuntime.Name; + return new AssemblyNameReference (nameDef.Name, nameDef.Version) { + HashAlgorithm = nameDef.HashAlgorithm, + PublicKeyToken = nameDef.PublicKeyToken, + }; + } +} + +sealed class VariableInfo { + public VariableInfo (Action load, Action loadAddress, Action store) + { + Load = load; + LoadAddress = loadAddress; + Store = store; + } + + public readonly Action Load; + public readonly Action LoadAddress; + public readonly Action Store; +} + +sealed class VariableDefinitions { + + Dictionary variables = new (); + Action Logger; + + public VariableDefinitions (AssemblyDefinition declaringAssembly, MethodDefinition declaringMethod, LambdaExpression expression, Action logger) + { + Logger = logger; + 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.Ldarga, 0), il => il.Emit (OpCodes.Starg, 0)); + break; + case 1: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_1), il => il.Emit (OpCodes.Ldarga, 1), il => il.Emit (OpCodes.Starg, 1)); + break; + case 2: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_2), il => il.Emit (OpCodes.Ldarga, 2), il => il.Emit (OpCodes.Starg, 2)); + break; + case 3: + v = new VariableInfo (il => il.Emit (OpCodes.Ldarg_3), il => il.Emit (OpCodes.Ldarga, 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.Ldarga, 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, Logger); + variableVisitor.Visit (e); + + Logger (TraceLevel.Verbose, $"# 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.Ldloca, 0), il => il.Emit (OpCodes.Stloc_0)); + break; + case 1: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_1), il => il.Emit (OpCodes.Ldloca, 1), il => il.Emit (OpCodes.Stloc_1)); + break; + case 2: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_2), il => il.Emit (OpCodes.Ldloca, 2), il => il.Emit (OpCodes.Stloc_2)); + break; + case 3: + v = new VariableInfo (il => il.Emit (OpCodes.Ldloc_3), il => il.Emit (OpCodes.Ldloca, 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.Ldloca, x), il => il.Emit (OpCodes.Stloc, x)); + break; + } + variables [c] = v; + if (c == variableVisitor.ReturnValue) { + ReturnValue = v; + } + Logger (TraceLevel.Verbose, $"# jonp: FillVariables: local var {c.Name} is index {i}"); + } + } +} + +class VariableExpressionVisitor : ExpressionVisitor { + + public VariableExpressionVisitor (ICollection arguments, Action logger) + { + Arguments = arguments; + Logger = logger; + } + + ICollection Arguments; + Action Logger; + + public List Variables = new (); + public ParameterExpression? ReturnValue; + + protected override Expression VisitGoto ( + GotoExpression node) + { + Logger (TraceLevel.Verbose, $"# jonp: VariableExpressionVisitor.Goto: {node}; node.Kind={node.Kind}; node.Type={node.Type}"); + if (node.Kind != GotoExpressionKind.Return) { + return base.VisitGoto (node); + } + if (ReturnValue != null) { + return base.VisitGoto (node); + } + Logger (TraceLevel.Verbose, $"# jonp: VariableExpressionVisitor.Goto: node.Target={node.Target} node.Value={node.Value}"); + if (node.Value is ParameterExpression rv) { + ReturnValue = rv; + return base.VisitGoto (node); + } + if (node.Type == typeof (void)) { + return base.VisitGoto (node); + } + var p = Expression.Parameter (node.Type, "__goto.Return.Temporary"); + Variables.Add (p); + ReturnValue = p; + Logger (TraceLevel.Verbose, $"# jonp: VariableExpressionVisitor.Goto: setting 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/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs index 3d117c508..f28d82268 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,17 @@ public void AddExportMethods () Assert.AreEqual ("action", methods [0].Name); Assert.AreEqual ("()V", methods [0].Signature); - Assert.IsTrue (methods [0].Marshaler is Action); - Assert.AreEqual ("staticAction", methods [1].Name); - Assert.AreEqual ("()V", methods [1].Signature); + Assert.AreEqual ("staticAction", methods [1].Name); + Assert.AreEqual ("()V", methods [1].Signature); + +#if NET + Assert.AreEqual ("_JniMarshal_PP_V", methods [0].Marshaler.GetType ().FullName); + Assert.AreEqual ("_JniMarshal_PP_V", methods [1].Marshaler.GetType ().FullName); +#else + Assert.IsTrue (methods [0].Marshaler is Action); Assert.IsTrue (methods [1].Marshaler is Action); +#endif // NET var m = t.GetStaticMethod ("testStaticMethods", "()V"); JniEnvironment.StaticMethods.CallStaticVoidMethod (t.PeerReference, m); @@ -201,6 +206,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 +227,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 +567,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..54e1f9998 --- /dev/null +++ b/tests/Java.Interop.Tools.Expressions-Tests/Java.Interop.Tools.ExpressionsTests/ExpressionAssemblyBuilderTests.cs @@ -0,0 +1,444 @@ +namespace Java.Interop.Tools.ExpressionsTests; + +using System.IO; +using System.Linq.Expressions; +using System.Text; + +using Java.Interop.Tools.Diagnostics; +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 { + 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) { + KeepTemporaryFiles = true, + }; + } + + [OneTimeTearDown] + public void TearDownTestEnvironment () + { + var path = Path.GetDirectoryName (typeof (ExpressionAssemblyBuilderTests).Assembly.Location) + ?? throw new InvalidOperationException ("`typeof (ExpressionAssemblyBuilderTests).Assembly.Location` is null?!"); + 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 CreateMarshalMethodDelegateType () + { + var t = ExpressionAssemblyBuilder!.CreateMarshalMethodDelegateType ( + "_Jonp_Demo", + new [] { + new ParameterDefinition ("jnienv", default, AssemblyDefinition!.MainModule.TypeSystem.IntPtr), + new ParameterDefinition ("klass", default, AssemblyDefinition!.MainModule.TypeSystem.IntPtr), + new ParameterDefinition ("value", default, AssemblyDefinition!.MainModule.TypeSystem.Int32), + }, + AssemblyDefinition.MainModule.TypeSystem.IntPtr + ); + AssemblyDefinition.MainModule.Types.Add (t); + } + + [Test] + public void Compile_MethodCall () + { + Expression e = () => Console.WriteLine ("constant"); + var m = ExpressionAssemblyBuilder!.Compile (e); + + AddMethod (m); + + var expected = new[]{ + "Instruction_0000: ldstr \"constant\"", + "Instruction_0001: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0002: 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_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_0025", + // } + // 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: castclass System.Exception", + "Instruction_0014: stloc.1", + "Instruction_0015: ldstr \"filtered\"", + "Instruction_0016: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0017: ldc.i4 3", + "Instruction_0018: stloc.0", + "Instruction_0019: leave Instruction_0025", + // } + // catch class System.Exception { + "Instruction_001a: castclass System.Exception", + "Instruction_001b: stloc.2", + "Instruction_001c: ldloc.2", + "Instruction_001d: callvirt System.String System.Object::ToString()", + "Instruction_001e: call System.Void System.Console::WriteLine(System.String)", + "Instruction_001f: ldc.i4 4", + "Instruction_0020: stloc.0", + "Instruction_0021: leave Instruction_0025", + // } + // finally { + "Instruction_0022: ldstr \"finally\"", + "Instruction_0023: call System.Void System.Console::WriteLine(System.String)", + "Instruction_0024: endfinally", + // } + "Instruction_0025: nop", + "Instruction_0026: nop", + "Instruction_0027: ldloc.0", + "Instruction_0028: 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..adf938648 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -1,17 +1,22 @@ using System; using System.Collections.Generic; +using System.Diagnostics; 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 System.Xml.Linq; 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,20 +28,37 @@ 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; - static public bool Verbose; + static public bool Verbose => Verbosity > 0; + static public int Verbosity; static bool keepTemporary; static bool forceRegeneration; static List typeNameRegexes = new List (); static string jvmDllPath; List FilesToDelete = new List (); + // AssemblyLoadContext loadContext; static string outDirectory; + static readonly string AppName; + + static App() + { + AppName = Path.GetFileNameWithoutExtension (Environment.GetCommandLineArgs () [0]); + var r = new ReaderParameters { + ReadSymbols = true, + InMemory = true, + }; + resolver = new DirectoryAssemblyResolver ( + logger: Log, + loadDebugSymbols: true, + loadReaderParameters: r + ); + } + public static int Main (string [] args) { var app = new App (); @@ -52,6 +74,34 @@ public static int Main (string [] args) return 0; } + static void Log (TraceLevel level, string message) + { + switch (level) { + case TraceLevel.Error: + ColorMessage ($"{AppName}: error: ", ConsoleColor.Red, Console.Error, writeLine: false); + ColorMessage (message, ConsoleColor.Red, Console.Error); + break; + case TraceLevel.Warning: + ColorMessage ($"{AppName}: warning: ", ConsoleColor.Yellow, Console.Error, writeLine: false); + ColorMessage (message, ConsoleColor.Yellow, Console.Error); + break; + case TraceLevel.Info: + if (Verbose) + ColorMessage (message, ConsoleColor.Cyan, Console.Out); + break; + case TraceLevel.Verbose: + if (Verbosity > 1) { + Console.WriteLine (message); + } + break; + default: + if (level == 0 || ((int) level) > Verbosity) { + Console.WriteLine (message); + } + break; + } + } + void AddMonoPathToResolverSearchDirectories () { var monoPath = Environment.GetEnvironmentVariable ("MONO_PATH"); @@ -111,9 +161,9 @@ List ProcessArguments (string [] args) { "t|type=", "Generate marshaling methods only for types whose names match {TYPE-REGEX}.", v => typeNameRegexes.Add (new Regex (v)) }, - { "v|verbose", + { "v|verbose:", "Output information about progress during the run of the tool", - v => Verbose = true }, + (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity + 1 }, new ResponseFileSource(), }; @@ -151,36 +201,71 @@ void ProcessAssemblies (List assemblies) { CreateJavaVM (jvmDllPath); - var readerParameters = new ReaderParameters { + var readerParameters = new ReaderParameters { AssemblyResolver = resolver, InMemory = true, ReadSymbols = true, - ReadWrite = string.IsNullOrEmpty (outDirectory), + ReadWrite = false, }; var readerParametersNoSymbols = new ReaderParameters { AssemblyResolver = resolver, InMemory = true, ReadSymbols = false, - ReadWrite = string.IsNullOrEmpty (outDirectory), + ReadWrite = false, + }; + + foreach (var r in references) { + resolver.SearchDirectories.Add (Path.GetDirectoryName (r)); + } + foreach (var assembly in assemblies) { + resolver.SearchDirectories.Add (Path.GetDirectoryName (assembly)); + } + var corlibDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (corlibDir != null) { + resolver.SearchDirectories.Add (corlibDir); + } + + // loadContext = CreateLoadContext (); + AppDomain.CurrentDomain.AssemblyResolve += (o, e) => { + Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {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) { if (!File.Exists (assembly)) { ErrorAndExit (Message.ErrorPathDoesNotExist, assembly); } + bool inPlaceUpdate = string.IsNullOrEmpty (outDirectory) || + string.Equals (Path.GetFullPath (outDirectory), Path.GetDirectoryName (Path.GetFullPath (assembly)), StringComparison.OrdinalIgnoreCase); + + readerParameters.ReadWrite = readerParametersNoSymbols.ReadWrite = inPlaceUpdate; - resolver.SearchDirectories.Add (Path.GetDirectoryName (assembly)); AssemblyDefinition ad; try { + if (inPlaceUpdate) { + File.Copy (assembly, assembly + ".orig"); + } ad = AssemblyDefinition.ReadAssembly (assembly, readerParameters); resolver.AddToCache (ad); } catch (Exception) { @@ -197,7 +282,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); } @@ -206,6 +290,9 @@ void ProcessAssemblies (List assemblies) void CreateJavaVM (string jvmDllPath) { + if (string.IsNullOrEmpty (jvmDllPath)) { + jvmDllPath = ReadJavaSdkDirectoryFromJdkInfoProps (); + } var builder = new JreRuntimeOptions { JvmLibraryPath = jvmDllPath, }; @@ -217,27 +304,72 @@ void CreateJavaVM (string jvmDllPath) } } - static JniRuntime.JniMarshalMemberBuilder CreateExportedMemberBuilder () + static string ReadJavaSdkDirectoryFromJdkInfoProps () { - return JniEnvironment.Runtime.MarshalMemberBuilder; - } + var location = typeof (App).Assembly.Location; // …/bin/Debug-net7.0/jnimarshalmethod-gen.dll + var binDir = Path.GetDirectoryName (Path.GetDirectoryName (location)) ?? Environment.CurrentDirectory; + var dirName = Path.GetFileName (Path.GetDirectoryName (location)); + if (binDir == null || dirName == null) { + return null; + } + if (!dirName.StartsWith ("Debug", StringComparison.OrdinalIgnoreCase) && + !dirName.StartsWith ("Release", StringComparison.OrdinalIgnoreCase)) { + return null; + } + var buildName = "Build" + dirName; + if (buildName.Contains ('-')) { + buildName = buildName.Substring (0, buildName.IndexOf ('-')); + } + var jdkPropFile = Path.Combine (binDir, buildName, "JdkInfo.props"); + if (!File.Exists (jdkPropFile)) { + return null; + } - static TypeBuilder GetTypeBuilder (ModuleBuilder mb, Type type) - { - 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 msbuild = XNamespace.Get ("http://schemas.microsoft.com/developer/msbuild/2003"); + + var jdkProps = XDocument.Load (jdkPropFile); + var jdkJvmPath = jdkProps.Elements () + .Elements (msbuild + "Choose") + .Elements (msbuild + "When") + .Elements (msbuild + "PropertyGroup") + .Elements (msbuild + "JdkJvmPath") + .FirstOrDefault (); + if (jdkJvmPath == null) { + return null; } + return jdkJvmPath.Value; + } - var tb = mb.DefineType (type.FullName, System.Reflection.TypeAttributes.Public); - definedTypes [type.FullName] = tb; + AssemblyLoadContext CreateLoadContext () + { + var c = new AssemblyLoadContext ("jnimarshalmethod-gen", isCollectible: true); + c.Resolving += (context, name) => { + Log (TraceLevel.Verbose, $"# 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; + } - return tb; + static JniRuntime.JniMarshalMemberBuilder CreateExportedMemberBuilder () + { + return JniEnvironment.Runtime.MarshalMemberBuilder; } class MethodsComparer : IComparer @@ -278,7 +410,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 +420,18 @@ 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, Log) { + KeepTemporaryFiles = keepTemporary, + }; + PrepareTypeMap (ad.MainModule); +// var assembly = loadContext.LoadFromStream (File.OpenRead (path)); + var assemblyBytes = File.ReadAllBytes (path); + var assembly = Assembly.Load (assemblyBytes); + Type[] types = null; try { types = assembly.GetTypes (); @@ -307,6 +439,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,9 +484,8 @@ 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; @@ -360,15 +494,27 @@ void CreateMarshalMethodAssembly (string path) Array.Sort (methods, new MethodsComparer (type, td)); 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 (); + 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 +534,63 @@ void CreateMarshalMethodAssembly (string path) continue; } - if (dt == null) - dt = GetTypeBuilder (dm, type); - - if (addedMethods.Contains (methodName)) + if (addedMethods.Contains (methodName)) { + Log (TraceLevel.Verbose, $"# jonp: method `{methodName}` 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 + Log (TraceLevel.Verbose, $"## 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); 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)) + if (!string.IsNullOrEmpty (outDirectory)) { + Directory.CreateDirectory (outDirectory); path = Path.Combine (outDirectory, Path.GetFileName (path)); + } + + assemblyBuilder.Write (path); - var mover = new TypeMover (dstAssembly, ad, path, definedTypes, resolver, cache); - mover.Move (); + // 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 +632,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; @@ -702,3 +826,36 @@ public static bool NeedsMarshalMethod (this MethodDefinition md, DirectoryAssemb } } } + +class _Jonp_ReferenceCodeGen +{ + static void A (IntPtr jnienv, IntPtr klass, int value) + { + JniRuntime jvm = JniEnvironment.Runtime; + JniRuntime.JniValueManager vm; + + var envp = new JniTransition (jnienv); + try { + vm = jvm.ValueManager; + vm.WaitForGCBridgeProcessing (); + } catch (Exception e) when (jvm.ExceptionShouldTransitionToJni (e)) { + envp.SetPendingException (e); + } finally { + envp.Dispose (); + } + } + + static void B () + { + } + + [JniAddNativeMethodRegistration] + static void RegisterNativeMethods (JniNativeMethodRegistrationArguments args) + { + var methods = new [] { + new JniNativeMethodRegistration ("a", "()V", new Action (A)), + new JniNativeMethodRegistration ("b", "()V", new Action (B)), + }; + args.AddRegistrations (methods); + } +} 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 @@ +