Skip to content

Commit d778204

Browse files
jonpryorjpobst
authored andcommitted
[Java.Interop] Better linker-friendly JavaObjectArray<T>.ValueMarshaler (#546)
Context: dotnet/android#3393 When attempting to execute the `Java.Interop-Tests.dll` unit tests within Xamarin.Android, some of the `JavaObjectArray<T>` unit tests would fail because the linker was removing the default constructor for the `JavaObjectArray<T>.ValueMarshaler` type, and that type was only created via `Activator.CreateInstance()` (aka "Reflection"): Test 'Java.InteropTests.JavaObjectArray_Int32ArrayArray_ContractTest.CollectionContract`1.Clear' failed: System.MissingMethodException : Default constructor not found for type Java.Interop.JavaObjectArray`1+ValueMarshaler[[System.Int32[], mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] at System.RuntimeType.CreateInstanceMono (System.Boolean nonPublic, System.Boolean wrapExceptions) at System.RuntimeType.CreateInstanceSlow (System.Boolean publicOnly, System.Boolean wrapExceptions, System.Boolean skipCheckThis, System.Boolean fillCache) at System.RuntimeType.CreateInstanceDefaultCtor (System.Boolean publicOnly, System.Boolean skipCheckThis, System.Boolean fillCache, System.Boolean wrapExceptions, System.Threading.StackCrawlMark& stackMark) [0x00027] at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic, System.Boolean wrapExceptions) at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic) at System.Activator.CreateInstance (System.Type type) at Java.Interop.JniRuntime+JniValueManager.GetValueMarshaler (System.Type type) at Java.Interop.JniRuntime+JniValueManager.GetValueMarshaler[T] () at Java.Interop.JavaObjectArray`1[T].SetElementAt (System.Int32 index, T value) at Java.Interop.JavaObjectArray`1[T]..ctor (System.Collections.Generic.IList`1[T] value) at Java.Interop.JavaObjectArray`1[T]..ctor (System.Collections.Generic.IEnumerable`1[T] value) at Java.InteropTests.JavaObjectArrayContractTest`1[T].CreateCollection (System.Collections.Generic.IEnumerable`1[T] values) at Cadenza.Collections.Tests.CollectionContract`1[T].Clear () In order for `runtime.ValueManager.GetValueMarshaler<T>()` to work in all cases, we need to ensure that the `JavaObjectArray<T>.ValueMarshaler` default constructor is preserved. How do we do that? There are two plausible ways to do that: 1. Introduce and use a `PreserveAttribute` type, or 2. Use constructs which are "IL friendly". [`PreserveAttribute`][0] is a custom attribute which the linker looks for "by name", not including the namespace, and allows a degree of control over linker behavior. While useful, it also has a tendency to be copied in numerous places -- Xamarin.Android and Xamarin.iOS both have copies of this type, in different namespaces! -- which in itself is not entirely desirable. Do we really want *another* copy of this type running around? `PreserveAttribute` also requires manual maintenance: you have to "know" it exists, and "know" how to use it, and "know" how it interacts with the linker, and if anything changes to invalidate that knowledge...there's ~nothing to verify that things are now wrong. This leaves the second solution -- use "IL friendly" constructs -- but in order to do so we need to know *how* to remove a Reflection-based `Activator.CreateInstance()` call with "something else" which does the "same" thing or better. Thus: Why are we using `Activator.CreateInstance()`? See also commit 77a6bf8, but: 1. We're within non-generic `JniRuntime.JniValueManager.GetValueMarshaler(System.Type)`, and 2. We know we need to return something that can marshal non-primitive arrays, and 3. We don't know, in terms of type parameters, what that type *is*. We need a `JavaObjectArray<T>.ValueMarshaler` instance, but `T` is provided by a `System.Type` instance. This is why we used `Activator.CreateInstance()`, so we could use: Activator.CreateInstance ( typeof (JavaObjectArray<>.ValueMarshaler) .MakeGenericType (elementType)); However, `Activator.CreateInstance()` is linker hostile, so how do we obtain a `JavaObjectArray<T>.ValueMarshaler` instance *without* reflection, while also allowing `T` to be specified at runtime? (This is where I'm thankful we're not a FullAOT environment...) In order to get the linker to preserve the constructor, the linker needs to "see" that the constructor is actually used. Do so: partial class JavaObjectArray<T> { internal static readonly ValueMarshaler Instance = new ValueMarshaler (); } That merely punts the problem. How do we get the linker to preserve the `JavaObjectArray<T>.Instance` field? We need an actual field ref: static JniValueMarshaler GetObjectArrayMarshalerHelper<T> () { return JavaObjectArray<T>.Instance; } That's not a complete solution, though: *something* needs to *statically* reference `GetObjectArrayMarshalerHelper<T>()` within IL so that the linker will preserve it. Otherwise, it'll be collected, which will cause the `JavaObjectArray<T>.Instance` field to be removed, as well as the `JavaObjectArray<T>.ValueMarshaler` constructor. How do we statically reference a generic method from a non-generic context? We do so by referencing it with type parameters specified, then use that reference to obtain the generic type definition of the method, then create a new method definition with our desired types. ...and we can do so (reasonably) sanely and (reasonably) efficiently by using delegates, which gives us a method via `ldftn`! Func<JniValueMarshaler> indirect = GetObjectArrayMarshalerHelper<object>; MethodInfo helperForObject = indirect.Method; MethodInfo helperForType = helperForObject.GetGenericMethodDefinition().MakeGenericMethod (elementType); Now that we have a `MethodInfo` for `GetObjectArrayMarshalerHelper<{elementType}>()`, we need only invoke it! Alas, `MethodInfo.Invoke()` is slow, so use more delegates! Func<JniValueMarshaler> direct = (Func<JniValueMarshaler>) Delegate.CreateDelegate (typeof (Func<JniValueMarshaler>), helperForType); return direct (); This allows us to have a reference to the generic `JavaObjectArray<T>.Instance` field "rooted" by the non-generic `JniValueMarshaler.GetValueMarshaler(Type)` method, in a way which is linker friendly and more efficient than `Activator.CreateInstance()`. [0]: https://docs.microsoft.com/en-us/dotnet/api/foundation.preserveattribute?view=xamarin-ios-sdk-12
1 parent 4bd07e0 commit d778204

File tree

2 files changed

+17
-1
lines changed

2 files changed

+17
-1
lines changed

src/Java.Interop/Java.Interop/JavaObjectArray.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ namespace Java.Interop
66
{
77
public class JavaObjectArray<T> : JavaArray<T>
88
{
9+
internal static readonly ValueMarshaler Instance = new ValueMarshaler ();
10+
911
public JavaObjectArray (ref JniObjectReference handle, JniObjectReferenceOptions transfer)
1012
: base (ref handle, transfer)
1113
{

src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ public JniValueMarshaler GetValueMarshaler (Type type)
521521
}
522522
}
523523

524-
return (JniValueMarshaler) Activator.CreateInstance (typeof (JavaObjectArray<>.ValueMarshaler).MakeGenericType (elementType));
524+
return GetObjectArrayMarshaler (elementType);
525525
}
526526

527527
if (typeof (IJavaPeerable).GetTypeInfo ().IsAssignableFrom (info)) {
@@ -544,6 +544,20 @@ public JniValueMarshaler GetValueMarshaler (Type type)
544544
return GetValueMarshalerCore (type);
545545
}
546546

547+
static JniValueMarshaler GetObjectArrayMarshaler (Type elementType)
548+
{
549+
Func<JniValueMarshaler> indirect = GetObjectArrayMarshalerHelper<object>;
550+
var reifiedMethodInfo = indirect.Method.GetGenericMethodDefinition ()
551+
.MakeGenericMethod (elementType);
552+
Func<JniValueMarshaler> direct = (Func<JniValueMarshaler>) Delegate.CreateDelegate (typeof (Func<JniValueMarshaler>), reifiedMethodInfo);
553+
return direct ();
554+
}
555+
556+
static JniValueMarshaler GetObjectArrayMarshalerHelper<T> ()
557+
{
558+
return JavaObjectArray<T>.Instance;
559+
}
560+
547561
protected virtual JniValueMarshaler GetValueMarshalerCore (Type type)
548562
{
549563
return ProxyValueMarshaler.Instance;

0 commit comments

Comments
 (0)