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)</_JavacSourceOptions>
   </PropertyGroup>
   <PropertyGroup>
-    <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll</_XamarinAndroidCecilPath>
     <XamarinAndroidToolsFullPath>$([System.IO.Path]::GetFullPath ('$(XamarinAndroidToolsDirectory)'))</XamarinAndroidToolsFullPath>
   </PropertyGroup>
 
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 @@
     <TestOutputFullPath>$(MSBuildThisFileDirectory)bin\Test$(Configuration)-$(TargetFramework.ToLowerInvariant())\</TestOutputFullPath>
     <UtilityOutputFullPath Condition=" '$(UtilityOutputFullPathCoreApps)' != '' ">$(UtilityOutputFullPathCoreApps)</UtilityOutputFullPath>
     <UtilityOutputFullPath Condition=" '$(UtilityOutputFullPathCoreApps)' == '' ">$(ToolOutputFullPath)</UtilityOutputFullPath>
+    <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPathCoreApps)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPathCoreApps)Xamarin.Android.Cecil.dll</_XamarinAndroidCecilPath>
     <RollForward>Major</RollForward>
     <JIUtilityVersion>$(JINetToolVersion)</JIUtilityVersion>
     <JICoreLibVersion>$(JINetCoreLibVersion)</JICoreLibVersion>
@@ -23,6 +24,7 @@
     <ToolOutputFullPath>$(MSBuildThisFileDirectory)bin\$(Configuration)\</ToolOutputFullPath>
     <TestOutputFullPath>$(MSBuildThisFileDirectory)bin\Test$(Configuration)\</TestOutputFullPath>
     <UtilityOutputFullPath Condition=" '$(UtilityOutputFullPath)' == '' ">$(ToolOutputFullPath)</UtilityOutputFullPath>
+    <_XamarinAndroidCecilPath Condition=" '$(CecilSourceDirectory)' != '' And Exists('$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll') ">$(UtilityOutputFullPath)Xamarin.Android.Cecil.dll</_XamarinAndroidCecilPath>
     <JIUtilityVersion>$(JIOldToolVersion)</JIUtilityVersion>
     <JICoreLibVersion>$(JIOldCoreLibVersion)</JICoreLibVersion>
   </PropertyGroup>
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 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>netstandard2.0;$(DotNetTargetFramework)</TargetFrameworks>
-    <LangVersion>8.0</LangVersion>
+    <LangVersion>9.0</LangVersion>
     <ProjectGuid>{B501D075-6183-4E1D-92C9-F7B5002475B1}</ProjectGuid>
     <Nullable>enable</Nullable>
     <SignAssembly>true</SignAssembly>
@@ -23,4 +23,4 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
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<Type> 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<string, Type>? marshalDelegateTypes;
 
 		static Type? CreateMarshalDelegateType (string name, Type? returnType, List<Type> 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 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DotNetTargetFramework)</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <Import Project="..\..\TargetFrameworkDependentValues.props" />
+
+  <PropertyGroup>
+    <OutputPath>$(UtilityOutputFullPath)</OutputPath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
+  </ItemGroup>
+
+  <Import Project="..\..\build-tools\scripts\cecil.projitems" />
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Java.Interop\Java.Interop.csproj" />
+    <ProjectReference Include="..\..\src\Java.Interop.Tools.Cecil\Java.Interop.Tools.Cecil.csproj" />
+    <ProjectReference Include="..\..\src\Java.Interop.Tools.Diagnostics\Java.Interop.Tools.Diagnostics.csproj" />
+  </ItemGroup>
+
+</Project>
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<TraceLevel, string> logger)
+	{
+		this.assemblyDef    = declaringAssembly;
+		this.body           = body;
+		this.variables      = variables;
+		il                  = body.GetILProcessor ();
+		Logger              = logger;
+	}
+
+	AssemblyDefinition                          assemblyDef;
+	MethodBody                                  body;
+	ILProcessor                                 il;
+	VariableDefinitions                         variables;
+	Dictionary<LabelTarget, List<Instruction>>  returnFixups    = new ();
+	Action<TraceLevel, string>                  Logger;
+
+	/// <summary>
+	/// Dispatches the expression to one of the more specialized visit methods in this class.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	[return: NotNullIfNotNull("node")]
+	public override Expression? Visit (
+			Expression? node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.Visit [{node?.NodeType.ToString () ?? "<null>"}]: {node}");
+		return base.Visit (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="BinaryExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+
+	/// <summary>
+	/// Visits the children of the <see cref="BlockExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="ConditionalExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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);
+	}
+
+	/// <summary>
+	/// Visits the <see cref="ConstantExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+	}
+
+	/// <summary>
+	/// Visits the <see cref="DebugInfoExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override  Expression VisitDebugInfo (
+			DebugInfoExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitDebugInfo: {node}");
+		return base.VisitDebugInfo (node);
+	}
+
+	/// <summary>
+	/// Visits the <see cref="DefaultExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitDefault (
+			DefaultExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitDefault: {node}");
+		return base.VisitDefault (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the extension expression.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	/// <remarks>
+	/// This can be overridden to visit or rewrite specific extension nodes.
+	/// If it is not overridden, this method will call <see cref="Expression.VisitChildren"/>,
+	/// which gives the node a chance to walk its children. By default,
+	/// <see cref="Expression.VisitChildren"/> will try to reduce the node.
+	/// </remarks>
+	protected override Expression VisitExtension (
+			Expression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitExtension: {node}");
+		return base.VisitExtension (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="GotoExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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<Instruction> 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;
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="InvocationExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitInvocation (
+			InvocationExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitInvocation: {node}");
+		return base.VisitInvocation (node);
+	}
+
+	/// <summary>
+	/// Visits the <see cref="LabelTarget"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	[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<Instruction> GetFixupsForLabelTarget (LabelTarget target)
+	{
+		if (!returnFixups.TryGetValue (target, out List<Instruction>? fixups)) {
+			returnFixups.Add (target, fixups = new ());
+		}
+		return fixups;
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="LabelExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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<Instruction>? 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);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="Expression{T}"/>.
+	/// </summary>
+	/// <typeparam name="T">The type of the delegate.</typeparam>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitLambda<T>(Expression<T> 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);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="LoopExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitLoop (
+			LoopExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitLoop: {node}");
+		return base.VisitLoop (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="IndexExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitIndex (
+			IndexExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitIndex: {node}");
+		return base.VisitIndex (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MethodCallExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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<string> cwl = Console.WriteLine;
+		il.Emit (OpCodes.Ldstr, message);
+		il.Emit (OpCodes.Call, assemblyDef.MainModule.ImportReference (cwl.Method));
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="NewArrayExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitNewArray (
+			NewArrayExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitNewArray: {node}");
+		return base.VisitNewArray (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="NewExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+	}
+
+	/// <summary>
+	/// Visits the <see cref="ParameterExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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;
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="RuntimeVariablesExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitRuntimeVariables (
+			RuntimeVariablesExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitRuntimeVariables: {node}");
+		return base.VisitRuntimeVariables (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="SwitchCase"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override SwitchCase VisitSwitchCase (
+			SwitchCase node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitSwitchCase: {node}");
+		return base.VisitSwitchCase (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="SwitchExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitSwitch (
+			SwitchExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitSwitch: {node}");
+		return base.VisitSwitch (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="CatchBlock"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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<Instruction>?  FixupLeaveOffsets;
+
+
+	/// <summary>
+	/// Visits the children of the <see cref="TryExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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 ());
+		}
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="TypeBinaryExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitTypeBinary (
+			TypeBinaryExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitTypeBinary: {node}");
+		return base.VisitTypeBinary (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="UnaryExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitUnary (
+			UnaryExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitUnary: {node}");
+		return base.VisitUnary (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberInitExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitMemberInit (
+			MemberInitExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMemberInit: {node}");
+		return base.VisitMemberInit (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="ListInitExpression"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override Expression VisitListInit (
+			ListInitExpression node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitListInit: {node}");
+		return base.VisitListInit (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="ElementInit"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override ElementInit VisitElementInit (
+			ElementInit node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitElementInit: {node}");
+		return base.VisitElementInit (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberBinding"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override MemberBinding VisitMemberBinding (
+			MemberBinding node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMemberBinding: {node}");
+		return base.VisitMemberBinding (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberAssignment"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override MemberAssignment VisitMemberAssignment (
+			MemberAssignment node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMemberAssignment: {node}");
+		return base.VisitMemberAssignment (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberMemberBinding"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override MemberMemberBinding VisitMemberMemberBinding (
+			MemberMemberBinding node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMemberMemberBinding: {node}");
+		return base.VisitMemberMemberBinding (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="MemberListBinding"/>.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	protected override MemberListBinding VisitMemberListBinding (
+			MemberListBinding node)
+	{
+		Logger (TraceLevel.Verbose, $"# jonp: CecilCompilerExpressionVisitor.VisitMemberListBinding: {node}");
+		return base.VisitMemberListBinding (node);
+	}
+
+	/// <summary>
+	/// Visits the children of the <see cref="DynamicExpression" />.
+	/// </summary>
+	/// <param name="node">The expression to visit.</param>
+	/// <returns>The modified expression, if it or any subexpression was modified;
+	/// otherwise, returns the original expression.</returns>
+	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<TraceLevel, string>? logger = null)
+	{
+		DeclaringAssemblyDefinition     = declaringAssemblyDefinition;
+		Logger                          = logger ?? Diagnostic.CreateConsoleLogger ();
+	}
+
+	public  AssemblyDefinition              DeclaringAssemblyDefinition     {get;}
+	public  Action<TraceLevel, string>      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<ExpressionMethodRegistration> 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<IEnumerable<JniNativeMethodRegistration>> 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<string> 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<ParameterDefinition> 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<ParameterDefinition> 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<ILProcessor> load, Action<ILProcessor> loadAddress, Action<ILProcessor> store)
+	{
+		Load            = load;
+		LoadAddress     = loadAddress;
+		Store           = store;
+	}
+
+	public  readonly    Action<ILProcessor> Load;
+	public  readonly    Action<ILProcessor> LoadAddress;
+	public  readonly    Action<ILProcessor> Store;
+}
+
+sealed class VariableDefinitions {
+
+	Dictionary<ParameterExpression, VariableInfo>   variables = new ();
+	Action<TraceLevel, string>                      Logger;
+
+	public VariableDefinitions (AssemblyDefinition declaringAssembly, MethodDefinition declaringMethod, LambdaExpression expression, Action<TraceLevel, string> 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<ParameterExpression> arguments, Action<TraceLevel, string> logger)
+	{
+		Arguments   = arguments;
+		Logger      = logger;
+	}
+
+	ICollection<ParameterExpression>    Arguments;
+	Action<TraceLevel, string>          Logger;
+
+	public  List<ParameterExpression>       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<IntPtr, IntPtr>);
 
-				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<IntPtr, IntPtr>);
 				Assert.IsTrue (methods [1].Marshaler is Action<IntPtr, IntPtr>);
+#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 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DotNetTargetFramework)</TargetFramework>
+    <RootNamespace>Java.Interop.Tools.ExpressionsTests</RootNamespace>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <Import Project="..\..\TargetFrameworkDependentValues.props" />
+
+  <PropertyGroup>
+    <OutputPath>$(TestOutputFullPath)</OutputPath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
+    <PackageReference Include="NUnit" />
+    <PackageReference Include="NUnit3TestAdapter" />
+  </ItemGroup>
+
+  <!--
+  <Import Project="..\..\build-tools\scripts\cecil.projitems" />
+  -->
+  <ItemGroup>
+    <PackageReference Include="Mono.Cecil" Version="0.11.4" />
+    <PackageReference Include="Mono.Linq.Expressions" />
+  </ItemGroup>
+
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Java.Interop.Tools.Expressions\Java.Interop.Tools.Expressions.csproj" />
+  </ItemGroup>
+
+</Project>
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<Action> 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<Func<int, int, bool>> 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<Func<int, int, int>> 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<Action>(() => Console.WriteLine ("try")).Body,
+				Expression.Return (target: exit, value: Expression.Constant (1), type: typeof (int))
+		);
+		var finallyBlock = E<Action>(() => Console.WriteLine ("finally")).Body;
+		var catchLog0   = E<Action<Exception>>(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<Action<Exception>>(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> {
+			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<int>),
+				body:           Expression.Block (variables: Array.Empty<ParameterExpression>(), expressions: block),
+				name:           nameof (Compile_TryCatchFinally),
+				tailCall:       false,
+				parameters:     Array.Empty<ParameterExpression>()
+		);
+
+		Assert.AreEqual (1, ((Func<int>) 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<TDelegate> E<TDelegate>(Expression<TDelegate> 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<Instruction> 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<Instruction> 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<Instruction> 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<string, TypeBuilder> definedTypes = new Dictionary<string, TypeBuilder> ();
 		static Dictionary<string, TypeDefinition> typeMap = new Dictionary<string, TypeDefinition> ();
 		static List<string> references = new List<string> ();
 		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<Regex> typeNameRegexes = new List<Regex> ();
 		static string jvmDllPath;
 		List<string> FilesToDelete = new List<string> ();
+		// 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<string> 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<string> 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<string> 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<string> 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<MethodInfo>
@@ -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 ("<default>", 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<Expression> ();
+				var registrations           = new List<ExpressionMethodRegistration> ();
 				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<JavaCallableAttribute> ();
+					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<Expression> 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<Action<JniNativeMethodRegistrationArguments>> (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<IntPtr, IntPtr, int> (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 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net472</TargetFramework>
+    <TargetFramework>$(DotNetTargetFramework)</TargetFramework>
     <OutputType>Exe</OutputType>
     <LangVersion>8.0</LangVersion>
     <AssemblyName>jnimarshalmethod-gen</AssemblyName>
@@ -13,6 +13,7 @@
 
   <PropertyGroup>
     <OutputPath>$(UtilityOutputFullPath)</OutputPath>
+    <_DumpRegisterNativeMembers>True</_DumpRegisterNativeMembers>
   </PropertyGroup>
 
   <PropertyGroup Condition=" '$(_DumpRegisterNativeMembers)' == 'True' ">
@@ -35,6 +36,7 @@
     <ProjectReference Include="..\..\src\Java.Interop\Java.Interop.csproj" />
     <ProjectReference Include="..\..\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj" />
     <ProjectReference Include="..\..\src\Java.Interop.Tools.Cecil\Java.Interop.Tools.Cecil.csproj" />
+    <ProjectReference Include="..\..\src\Java.Interop.Tools.Expressions\Java.Interop.Tools.Expressions.csproj" />
   </ItemGroup>
 
 </Project>