Skip to content

Commit 1a2eb95

Browse files
radekdoulikjonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] jnimarshalmethod-gen.exe integration (#2153)
Context: https://github.com/xamarin/xamarin-android/projects/1 Context: #2138 A "JNI Marshal Method" is a method which the JVM eventually executes when a Java `native` method is invoked. JNI Marshal Methods are currently contained within binding assemblies for all `virtual` methods, e.g. `Java.Lang.Object` contains: partial class Object { static int n_GetHashCode (IntPtr jnienv, IntPtr native__this) { var __this = Object.GetObject<Object> (jnienv, native__this, JniHandleOwnership.DoNotTransfer); return __this.GetHashCode (); } } If a C# class overrides `Java.Lang.Object.GetHashCode()`, then `Object.n_GetHashCode()` will be invoked when Java code calls `value.hashCode()` on an instance of that C# class. JNI Marshal Methods are responsible for marshaling parameters and return types. However, there is one problem with JNI Marshal Methods as currently constructed: they *require* the use of `System.Reflection.Emit`, via `Android.Runtime.JNINativeWrapper.CreateDelegate()`. `Object.n_GetHashCode()` isn't *directly* registered with JNI. Instead, a "wrapper" is generated at runtime, and it's the wrapper which is registered with JNI. The wrapper effectively does: int wrapper_n_GetHashCode (IntPtr jnienv, IntPtr native__this) { try { JNIEnv.WaitForBridgeProcessing(); return n_GetHashCode (jnienv, native__this); } catch (Exception e) when Debugger.IsAttached || !JNIEnv.PropagateExceptions { AndroidEnvironment.UnhandledException (e); } } Previously, the use of `DynamicMethod` and `System.Reflection.Emit` was unavoidable, as we needed to ensure that all exceptions were handled appropriately, no matter which `generator` version was used to generate the JNI Marshal Methods. Enter `jnimarshalmethod-gen.exe`, which is a utility which will generate "complete" JNI Marshal Methods that can be registered directly with JNI, *without* using `JNINativeWrapper` or requiring use of `DynamicMethod`. `jnimarshalmethod-gen.exe` would process the hypothetical C# `GetHashCode()` override and generate the JNI Marshal Method: partial class ExampleObjectSubclass : Java.Lang.Object { public override int GetHashCode () {return 42;} /* generated by `jnimarshalmethod-gen.exe` */ partial class '__<$>_jni_marshal_methods' { public int GetHashCode (IntPtr __jnienv, IntPtr native__this) { var jniTransition = new JniTransition (__jnienv); JniRuntime runtime = default; try { runtime = JniEnvironment.Runtime; var valueManager = runtime.ValueManager; valueManager.WaitForGCBridgeProcessing (); var __this = valueManager.GetValue<ExampleObjectSubclass>(native__this); return __this.GetHashCode (); } catch (Exception e) when (runtime.ExceptionShouldTransitionToJni (ex)) { jniTransition.SetPendingException (ex); } finally { jniTransition.Dispose (); } } } } The eventual hope and intent is that this will improve process startup times, as we'll need to do less work. This is an initial effort, and not yet complete. `jnimarshalmethod-gen.exe` is invoked from the new `_GenerateJniMarshalMethods` target, which uses *xamarin-android*'s mono, *not* a system mono or other managed runtime. This is done so that `$MONO_PATH` can be overridden, allowing "normal" use of System.Reflection *by `JniValueMarshaler` instances* during *build* time, *not* runtime. The `$(AndroidGenerateJniMarshalMethodsAdditionalArguments)` MSBuild property can be used to add additional parameters to the `jnimarshalmethod-gen.exe` invocation. This is useful for debugging, so that options such as `-v`, `-d`, or `--keeptemp` can be used. Enable use of `jnimarshalmethod-gen.exe` for user assemblies and `Mono.Android.dll` by setting `$(AndroidGenerateJniMarshalMethods)` to True. This is currently only supported on non-Windows platforms. Additionally, remove the `NotImplementedException` throws from the `Android.Runtime.AndroidValueManager`. `AndroidValueManager` is now used from the generated marshal methods, which handle primitive arrays. In order to make this whole process work, some additional custom value marshalers are needed. Add custom JNI value marshalers for `Android.Graphics.Color` and `Android.Runtime.IJavaObject`. Additional examples `jnimarshalmethod-gen.exe` output: using System; using Java.Interop; using Android.Runtime; public static void n_setAdapter_Landroid_widget_ListAdapter_ (IntPtr __jnienv, IntPtr __this, IntPtr value) { JniTransition jniTransition = new JniTransition (__jnienv); JniRuntime runtime = default(JniRuntime); try { runtime = JniEnvironment.Runtime; JniRuntime.JniValueManager valueManager = runtime.ValueManager; valueManager.WaitForGCBridgeProcessing (); AbsListView value2 = valueManager.GetValue<AbsListView> (__this); IListAdapter listAdapter2 = value2.Adapter = Java.Interop.JavaConvert.FromJniHandle<IListAdapter> (value, JniHandleOwnership.DoNotTransfer); } catch (Exception ex) when (runtime.ExceptionShouldTransitionToJni (ex)) { jniTransition.SetPendingException (ex); } finally { jniTransition.Dispose (); } } public static IntPtr n_getAdapter (IntPtr __jnienv, IntPtr __this) { JniTransition jniTransition = new JniTransition (__jnienv); JniRuntime runtime = default(JniRuntime); try { runtime = JniEnvironment.Runtime; JniRuntime.JniValueManager valueManager = runtime.ValueManager; valueManager.WaitForGCBridgeProcessing (); AbsListView value = valueManager.GetValue<AbsListView> (__this); IListAdapter adapter = value.Adapter; return JNIEnv.ToLocalJniHandle (adapter); } catch (Exception ex) when (runtime.ExceptionShouldTransitionToJni (ex)) { jniTransition.SetPendingException (ex); return default(IntPtr); } finally { jniTransition.Dispose (); } IntPtr intPtr = default(IntPtr); return intPtr; } Profiling results of the Xamarin.Forms Integration Test running on Pixel 2 XL phone: Old marshaling: 133 1 15 Android.Runtime.JNIEnv:RegisterJniNatives (intptr,int,intptr,intptr,int) 288 2 1 Android.Runtime.JNIEnv:Initialize (Android.Runtime.JnienvInitializeArgs*) New marshaling: 68 1 15 Android.Runtime.JNIEnv:RegisterJniNatives (intptr,int,intptr,intptr,int) 264 2 1 Android.Runtime.JNIEnv:Initialize (Android.Runtime.JnienvInitializeArgs*) Native member registration for a type is ~2x faster and `JNIEnv.Initialize()` is ~20ms faster. Finally, `RunJavaInteropTests` was moved from `@(_RunParallelTestTarget)` item group to the `@(_RunTestTarget)` group, because it overwrites `Java.Runtime.Environment.dll.config`, which is required by `jnimarshalmethod-gen.exe` to run. We need to run these tests after the `.apk` tests. This should be fixed in the future, hopefully by adding and/or fixing the Inputs/Outputs of the relevant targets.
1 parent 0d6e65a commit 1a2eb95

File tree

15 files changed

+208
-31
lines changed

15 files changed

+208
-31
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)..\..\bin\Build$(Configuration)\xa-prep-tasks.dll" TaskName="Xamarin.Android.BuildTools.PrepTasks.ReplaceFileContents" />
4+
<Target Name="_CreateJavaInteropDllConfigs"
5+
Inputs="$(XAInstallPrefix)xbuild\Xamarin\Android\Java.Interop.dll;$(JavaInteropSourceDirectory)\src\Java.Runtime.Environment\Java.Runtime.Environment.dll.config"
6+
Outputs="$(XAInstallPrefix)xbuild\Xamarin\Android\Java.Interop.dll.config;$(XAInstallPrefix)xbuild\Xamarin\Android\Java.Runtime.Environment.dll.config">
7+
<ReadLinesFromFile
8+
File="$(MSBuildThisFileDirectory)java-interop.dllmap">
9+
<Output TaskParameter="Lines" ItemName="_JavaInteropDllMapContent" />
10+
</ReadLinesFromFile>
11+
<WriteLinesToFile
12+
File="$(XAInstallPrefix)xbuild\Xamarin\Android\Java.Interop.dll.config"
13+
Lines="&lt;configuration&gt;;@(_JavaInteropDllMapContent);&lt;/configuration&gt;"
14+
Overwrite="True"
15+
/>
16+
<PropertyGroup>
17+
<_DllMaps>@(_JavaInteropDllMapContent->'%(Identity)', '%0a ')</_DllMaps>
18+
</PropertyGroup>
19+
<ReplaceFileContents
20+
Condition="Exists('$(JavaInteropSourceDirectory)\src\Java.Runtime.Environment\Java.Runtime.Environment.dll.config')"
21+
SourceFile="$(JavaInteropSourceDirectory)\src\Java.Runtime.Environment\Java.Runtime.Environment.dll.config"
22+
DestinationFile="$(XAInstallPrefix)xbuild\Xamarin\Android\Java.Runtime.Environment.dll.config"
23+
Replacements="&lt;configuration&gt;=&lt;configuration&gt;%0a $(_DllMaps)"
24+
/>
25+
</Target>
26+
</Project>

build-tools/scripts/RunTests.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@
147147
</Target>
148148
<ItemGroup>
149149
<_RunParallelTestTarget Include="RunNUnitTests" />
150-
<_RunParallelTestTarget Include="RunJavaInteropTests" />
151150
<_RunParallelTestTarget Include="RunApkTests" />
152151
</ItemGroup>
153152
<ItemGroup>
153+
<_RunTestTarget Include="RunJavaInteropTests" />
154154
<_RunTestTarget Include="RunPerformanceTests" />
155155
</ItemGroup>
156156
<Target Name="RunAllTests">

external/Java.Interop

src/Mono.Android/Android.Graphics/Color.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
46
using System.Text;
57

68
using Android.Runtime;
79

10+
using Java.Interop;
11+
using Java.Interop.Expressions;
12+
813
namespace Android.Graphics
914
{
15+
[JniValueMarshaler (typeof (ColorValueMarshaler))]
1016
public struct Color
1117
{
1218
private int color;
@@ -386,4 +392,50 @@ public static void RGBToHSV (int red, int green, int blue, float[] hsv)
386392
public static Color YellowGreen { get { return new Color (0xFF9ACD32); } }
387393
#endregion
388394
}
395+
396+
public class ColorValueMarshaler : JniValueMarshaler<Color>
397+
{
398+
public override Type MarshalType {
399+
get { return typeof (int); }
400+
}
401+
402+
public override Color CreateGenericValue (ref JniObjectReference reference, JniObjectReferenceOptions options, Type targetType)
403+
{
404+
throw new NotImplementedException ();
405+
}
406+
407+
public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (Color value, ParameterAttributes synchronize)
408+
{
409+
throw new NotImplementedException ();
410+
}
411+
412+
public override void DestroyGenericArgumentState (Color value, ref JniValueMarshalerState state, ParameterAttributes synchronize)
413+
{
414+
throw new NotImplementedException ();
415+
}
416+
417+
public override Expression CreateParameterToManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize, Type targetType)
418+
{
419+
var c = typeof (Color).GetConstructor (new[]{typeof (int)});
420+
var v = Expression.Variable (typeof (Color), sourceValue.Name + "_val");
421+
context.LocalVariables.Add (v);
422+
context.CreationStatements.Add (Expression.Assign (v, Expression.New (c, sourceValue)));
423+
424+
return v;
425+
}
426+
427+
public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize)
428+
{
429+
var r = Expression.Variable (MarshalType, sourceValue.Name + "_p");
430+
context.LocalVariables.Add (r);
431+
context.CreationStatements.Add (Expression.Assign (r, Expression.Field (sourceValue, "color")));
432+
433+
return r;
434+
}
435+
436+
public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue)
437+
{
438+
return CreateParameterFromManagedExpression (context, sourceValue, 0);
439+
}
440+
}
389441
}

src/Mono.Android/Android.Runtime/AndroidRuntime.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,10 @@ public override void WaitForGCBridgeProcessing ()
363363

364364
public override void AddPeer (IJavaPeerable value)
365365
{
366-
throw new NotImplementedException ();
367366
}
368367

369368
public override void RemovePeer (IJavaPeerable value)
370369
{
371-
throw new NotImplementedException ();
372370
}
373371

374372
public override IJavaPeerable PeekPeer (JniObjectReference reference)
@@ -378,17 +376,15 @@ public override IJavaPeerable PeekPeer (JniObjectReference reference)
378376

379377
public override void CollectPeers ()
380378
{
381-
throw new NotImplementedException ();
382379
}
383380

384381
public override void FinalizePeer (IJavaPeerable value)
385382
{
386-
throw new NotImplementedException ();
387383
}
388384

389385
public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
390386
{
391-
throw new NotImplementedException ();
387+
return null;
392388
}
393389
}
394390
}

src/Mono.Android/Android.Runtime/IJavaObject.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Android.Runtime {
44

5+
[Java.Interop.JniValueMarshaler (typeof (IJavaObjectValueMarshaler))]
56
public interface IJavaObject : IDisposable {
67
IntPtr Handle { get; }
78
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Linq;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
6+
using Java.Interop;
7+
using Java.Interop.Expressions;
8+
9+
namespace Android.Runtime
10+
{
11+
sealed class IJavaObjectValueMarshaler : JniValueMarshaler<IJavaObject> {
12+
13+
internal static IJavaObjectValueMarshaler Instance = new IJavaObjectValueMarshaler ();
14+
15+
public override IJavaObject CreateGenericValue (ref JniObjectReference reference, JniObjectReferenceOptions options, Type targetType)
16+
{
17+
throw new NotImplementedException ();
18+
}
19+
20+
public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (IJavaObject value, ParameterAttributes synchronize)
21+
{
22+
throw new NotImplementedException ();
23+
}
24+
25+
public override void DestroyGenericArgumentState (IJavaObject value, ref JniValueMarshalerState state, ParameterAttributes synchronize)
26+
{
27+
throw new NotImplementedException ();
28+
}
29+
30+
public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue)
31+
{
32+
return Expression.Call (
33+
typeof (JNIEnv),
34+
"ToLocalJniHandle",
35+
null,
36+
sourceValue);
37+
}
38+
39+
public override Expression CreateParameterToManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize, Type targetType)
40+
{
41+
var r = Expression.Variable (targetType, sourceValue.Name + "_val");
42+
context.LocalVariables.Add (r);
43+
context.CreationStatements.Add (
44+
Expression.Assign (r,
45+
Expression.Call (
46+
typeof (JavaConvert),
47+
"FromJniHandle",
48+
new[]{targetType},
49+
sourceValue,
50+
Expression.Field (null, typeof (JniHandleOwnership), "DoNotTransfer"))));
51+
return r;
52+
}
53+
}
54+
}

src/Mono.Android/Mono.Android.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
<Link>JavaNativeTypeManager.cs</Link>
8484
</Compile>
8585
<Compile Include="Android.Runtime\DynamicMethodNameCounter.cs" />
86+
<Compile Include="Android.Runtime\IJavaObjectValueMarshaler.cs" />
8687
</ItemGroup>
8788
<Import Project="..\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems" Label="Shared" Condition="Exists('..\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems')" />
8889
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />

src/Mono.Android/Test/Mono.Android-Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
<ConsolePause>false</ConsolePause>
4444
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
4545
<AndroidLinkTool Condition=" '$(AndroidLinkTool)' == '' ">r8</AndroidLinkTool>
46+
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
4647
</PropertyGroup>
4748
<ItemGroup>
4849
<Reference Include="System" />

src/Mono.Android/Test/Mono.Android-Tests.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</ItemGroup>
99
<Import Project="Mono.Android-Tests.projitems" />
1010
<Import Project="..\..\..\build-tools\scripts\TestApks.targets" />
11+
<Import Project="..\..\..\build-tools\scripts\JavaInteropDllConfigs.targets" />
1112
<Target Name="BuildNativeLibs"
1213
BeforeTargets="Build"
1314
DependsOnTargets="AndroidPrepareForBuild"
@@ -16,4 +17,8 @@
1617
<Error Text="Could not locate Android NDK." Condition="!Exists ('$(NdkBuildPath)')" />
1718
<Exec Command="&quot;$(NdkBuildPath)&quot;" />
1819
</Target>
20+
<Target Name="EnsureJavaInteropDllConfigs"
21+
BeforeTargets="_GenerateJniMarshalMethods"
22+
DependsOnTargets="_CreateJavaInteropDllConfigs">
23+
</Target>
1924
</Project>

0 commit comments

Comments
 (0)