-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Java.Interop] Avoid Type.GetType()
in ManagedPeer
#1168
Conversation
Fixes: #1165 Context: #1153 Context: #1157 When building for NativeAOT (#1153) or when building .NET Android apps with `-p:IsAotcompatible=true` (#1157), we get [IL2057][0] warnings from `ManagedPeer.cs`: ManagedPeer.cs(93,19,93,112): warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type. ManagedPeer.cs(156,18,156,65): warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type. ManagedPeer.cs(198,35,198,92): warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type. These warnings are because `ManagedPeer.Construct()` and `ManagedPeer.RegisterNativeMembers()` use `Type.GetType()` on string values provided *from Java code*, and thus the IL trimmer does not have visibility into those strings, and thus cannot reliably determine which types need to be preserved: // Java Callable Wrapper /* partial */ class ManagedType { public static final String __md_methods; static { __md_methods = "n_GetString:()Ljava/lang/String;:__export__\n" + ""; net.dot.jni.ManagedPeer.registerNativeMembers ( /* nativeClass */ ManagedType.class, /* assemblyQualifiedName */ "Example.ManagedType, Hello-NativeAOTFromJNI", /* methods */ __md_methods); } public ManagedType (int p0) { super (); if (getClass () == ManagedType.class) { net.dot.jni.ManagedPeer.construct ( /* self */ this, /* assemblyQualifiedName */ "Example.ManagedType, Hello-NativeAOTFromJNI", /* constructorSignature */ "System.Int32, System.Runtime", /* arguments */ new java.lang.Object[] { p0 }); } } } `ManagedPeer.construct()` passes *two* sets of assembly-qualified type names: `assemblyQualifiedName` contains the type to construct, while `constructorSignature` contains a `:`-separated list of assembly-qualified type names for the constructor parameters. Each of these are passed to `Type.GetType()`. `ManagedPeer.registerNativeMembers()` passes an assembly-qualified type name to `ManagedPeer.RegisterNativeMembers()`, which passes the assembly-qualified type name to `Type.GetType()` to find the type to register native methods for. If we more strongly rely on JNI signatures, we can remove the need for Java Callable Wrappers to contain assembly-qualified type names entirely, thus removing the need for `ManagedPeer` to use `Type.GetType()`, removing the IL2057 warnings. For `ManagedPeer.construct()`, `assemblyQualifiedName` can be replaced with getting the JNI type signature from `self.getClass()`, and `constructorSignature` can be replaced with a *JNI method signature* of the calling constructor. For `ManagedPeer.registerNativeMembers()`, `assemblyQualifiedName` can be replaced with getting the JNI type signature from `nativeClass`. // Java Callable Wrapper /* partial */ class ManagedType { public static final String __md_methods; static { __md_methods = "n_GetString:()Ljava/lang/String;:__export__\n" + ""; net.dot.jni.ManagedPeer.registerNativeMembers ( /* nativeClass */ ManagedType.class, /* methods */ __md_methods); } public ManagedType (int p0) { super (); if (getClass () == ManagedType.class) { net.dot.jni.ManagedPeer.construct ( /* self */ this, /* constructorSignature */ "(I)V", /* arguments */ new java.lang.Object[] { p0 }); } } } Furthermore, if we add `[DynamicallyAccessedMembers]` to `JniRuntime.JniTypeManager.GetType()`, we can fix some [IL2075][1] warnings which appeared after fixing the IL2057 warnings. Aside: Excising assembly-qualified type names from Java Callable Wrappers had some "interesting" knock-on effects in the unit tests, requiring that more typemap information be explicitly provided. (This same information was *implicitly* provided before, via the provision of assembly-qualified type names everywhere…) [0]: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/IL2057 [1]: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/il2075
0fab1d3
to
b75ad19
Compare
There is a remaining problem with this approach: there is no requirement of a 1:1 mapping between Java types and managed types. There are already scenarios in which the mapping is ambiguous, and the pre-existing solution of using assembly-qualified type names resolved the ambiguity: arrays. Consider Java This will be addressed in a future commit to this PR. |
Context: #1168 (comment) > There is a remaining problem with this approach: there is no > requirement of a 1:1 mapping between Java types and managed types. A useful example of that is with arrays: a Java `int[]` array can be treated as one of the following types, in various contexts: * C# `int[]` * `JavaArray<int>` * `JavaPrimitiveArray<int>` * `JavaInt32Array` Update `JavaCallableExample` to demonstrate this: partial class JavaCallableExample { [JavaCallableConstructor(SuperConstructorExpression="")] public JavaCallableExample (int[] a, JavaInt32Array b); } The intention is twofold: 1. This should result in a Java Callable Wrapper constructor with signature `JavaCallableExample(int[] p0, int[] p1)`, and 2. Java code should be able to invoke this constructor. Turns out, neither of these worked when `Type.GetType()` is not used for constructor argument lookup: `JavaCallableWrapperGenerator` didn't fully support e.g. `[JniTypeSignature("I", ArrayRank=1)]`, so didn't know what to do with `JavaInt32Array`. Once (1) was fixed, (2) would fail because `JniRuntime.JniTypeManager.GetType(JniTypeSignature.Parse("[I"))` would return `JavaPrimitiveArray<int>`, which wasn't used in `JavaCallableExample`, resulting in: System.NotSupportedException : Unable to find constructor Java.InteropTests.JavaCallableExample(Java.Interop.JavaPrimitiveArray`1[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], Java.Interop.JavaPrimitiveArray`1[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]). Please provide the missing constructor. ----> Java.Interop.JniLocationException : Exception of type 'Java.Interop.JniLocationException' was thrown. Stack Trace: at Java.Interop.ManagedPeer.GetConstructor(JniTypeManager typeManager, Type type, String signature, Type[]& parameterTypes) at Java.Interop.ManagedPeer.Construct(IntPtr jnienv, IntPtr klass, IntPtr n_self, IntPtr n_constructorSignature, IntPtr n_constructorArguments) … --- End of managed Java.Interop.JavaException stack trace --- java.lang.Throwable at net.dot.jni.ManagedPeer.construct(Native Method) at net.dot.jni.test.JavaCallableExample.<init>(JavaCallableExample.java:32) at net.dot.jni.test.UseJavaCallableExample.test(UseJavaCallableExample.java:8) Intent (2) had two causes: 1. Using `JniRuntime.JniTypeManager.GetType()` can only return a single type, but there are multiple possible matches. Thus, we need to instead use `JniRuntime.JniTypeManager.GetTypes()`. 2. `JniRuntime.JniTypeManager.GetTypes()` was incomplete, which is a longstanding limitation from f60906c: for `[I`, it would only return `JavaPrimitiveArray<int>` and `int[]`, in that order. Fix both of these. `JniRuntime.JniTypeManager.GetTypes(JniTypeSignature.Parse("[I"))` will now include: * `JavaArray<int>` * `JavaPrimitiveArray<int>` * `JavaInt32Array` * `int[]` This now allows the `JavaCallableExample` constructor to be invoked from Java. Because `ManagedPeer.Construct()` is now doing so much extra work in order to find the `ConstructorInfo` to invoke, cache the lookups. (Technically this is a "memory leak," as cache entries are never removed.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My general questions about performance depend on how often these would be called at runtime by a MAUI app?
If it's at build time -- or rare cases, we probably won't even notice these.
foreach (var e in JniPrimitiveArrayTypes) { | ||
if (Array.IndexOf (e.ArrayTypes, type) < 0) | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this happen on device? This looks like it is O(N^2). Is there a way to rework the data to be a Dictionary<Type, JniPrimitiveArrayInfo>
to make this lookup faster? I guess you'd also want the keys to map to duplicate entries as the values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not, and we're talking O(N**2) on 8 entries, each containing 4 elements within e.ArrayTypes
, or 32 entries total. O(N**2) on 32 entries should be negligible.
Is there a way to rework the data to be a Dictionary<Type, JniPrimitiveArrayInfo> to make this lookup faster?
Yes, but as this is static data, it'll be constructed during app startup, even for .NET Android (unless we make it conditional, and please no?), so I'm trying to figure out how to minimize startup overhead.
My assumption (un-timed) is that creating an array of structs will be faster than populating a Dictionary, particularly when we're dealing with so few elements (8).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, about that init vs. lookup time…
Repro App
```csharpusing System.Diagnostics;
const int CreationCount = 1000000;
const int LookupCount = 1000000;
var a = Stopwatch.StartNew ();
for (int i = 0; i < CreationCount; ++i) {
CreateArray ();
}
a.Stop ();
var d = Stopwatch.StartNew ();
for (int i = 0; i < CreationCount; ++i) {
CreateDict ();
}
d.Stop ();
Console.WriteLine ($"Array Creation: {a.ElapsedMilliseconds}ms");
Console.WriteLine ($" Dict Creation: {d.ElapsedMilliseconds}ms");
var av = CreateArray ();
a = Stopwatch.StartNew ();
for (int i = 0; i < LookupCount; ++i) {
TryArrayLookup (av, typeof (int));
}
a.Stop ();
var bv = CreateDict ();
for (int i = 0; i < LookupCount; ++i) {
TryDictLookup (bv, typeof (int));
}
d.Stop ();
Console.WriteLine ($"Array Lookup: {a.ElapsedMilliseconds}ms");
Console.WriteLine ($" Dict Lookup: {d.ElapsedMilliseconds}ms");
static JniPrimitiveArrayInfo[] CreateArray ()
{
return new JniPrimitiveArrayInfo[]{
new ("Z", typeof (Boolean), typeof (Boolean[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaBooleanArray)),
new ("B", typeof (SByte), typeof (SByte[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaSByteArray)),
new ("C", typeof (Char), typeof (Char[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaCharArray)),
new ("S", typeof (Int16), typeof (Int16[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt16Array)),
new ("I", typeof (Int32), typeof (Int32[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt32Array)),
new ("J", typeof (Int64), typeof (Int64[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt64Array)),
new ("F", typeof (Single), typeof (Single[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaSingleArray)),
new ("D", typeof (Double), typeof (Double[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaDoubleArray)),
};
}
static bool TryArrayLookup (JniPrimitiveArrayInfo[] array, Type type)
{
foreach (var e in array) {
if (Array.IndexOf (e.ArrayTypes, type) < 0)
continue;
return true;
}
return false;
}
static Dictionary<Type, JniPrimitiveArrayInfo> CreateDict ()
{
return new Dictionary<Type, JniPrimitiveArrayInfo> () {
[typeof (Boolean)] = new ("Z", typeof (Boolean), typeof (Boolean[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaBooleanArray)),
[typeof (SByte) ] = new ("B", typeof (SByte), typeof (SByte[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaSByteArray)),
[typeof (Char) ] = new ("C", typeof (Char), typeof (Char[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaCharArray)),
[typeof (Int16) ] = new ("S", typeof (Int16), typeof (Int16[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt16Array)),
[typeof (Int32) ] = new ("I", typeof (Int32), typeof (Int32[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt32Array)),
[typeof (Int64) ] = new ("J", typeof (Int64), typeof (Int64[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaInt64Array)),
[typeof (Single) ] = new ("F", typeof (Single), typeof (Single[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaSingleArray)),
[typeof (Double) ] = new ("D", typeof (Double), typeof (Double[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (JavaDoubleArray)),
};
}
static bool TryDictLookup (Dictionary<Type, JniPrimitiveArrayInfo> dict, Type type)
{
foreach (var v in dict.Values) {
if (Array.IndexOf (v.ArrayTypes, type) < 0)
return true;
}
return false;
}
class JavaArray {}
class JavaPrimitiveArray : JavaArray {}
class JavaBooleanArray : JavaPrimitiveArray {}
class JavaSByteArray : JavaPrimitiveArray {}
class JavaCharArray : JavaPrimitiveArray {}
class JavaInt16Array : JavaPrimitiveArray {}
class JavaInt32Array : JavaPrimitiveArray {}
class JavaInt64Array : JavaPrimitiveArray {}
class JavaSingleArray : JavaPrimitiveArray {}
class JavaDoubleArray : JavaPrimitiveArray {}
readonly struct JniPrimitiveArrayInfo {
public readonly JniTypeSignature JniTypeSignature;
public readonly Type PrimitiveType;
public readonly Type[] ArrayTypes;
public JniPrimitiveArrayInfo (string jniSimpleReference, Type primitiveType, params Type[] arrayTypes)
{
JniTypeSignature = new JniTypeSignature (jniSimpleReference, arrayRank: 1, keyword: true);
PrimitiveType = primitiveType;
ArrayTypes = arrayTypes;
}
}
struct JniTypeSignature {
public string SimpleName;
public int ArrayRank;
public bool Keyword;
public JniTypeSignature (string simpleName, int arrayRank, bool keyword)
{
SimpleName = simpleName;
ArrayRank = arrayRank;
Keyword = keyword;
}
}
</details>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently <details/>
doesn't work like i thought, so here's the execution + summary:
Results:
% dotnet run
Array Creation: 646ms
Dict Creation: 1131ms
Array Lookup: 29ms
Dict Lookup: 1131ms
% dotnet run -c Release
Array Creation: 389ms
Dict Creation: 770ms
Array Lookup: 21ms
Dict Lookup: 770ms
As expected, array lookup is faster -- half the time! -- and lookup is also much faster with arrays.
That said, this investigation did present a bug in GetBuiltInTypeArraySignature()
: it shouldn't have been looking at .ArrayTypes
, but instead .PrimitiveType
, because the callsite in GetTypeSignature(Type)
"unwraps" arrays, which kinda makes that particular codepath unusable, as in GetBuiltInTypeArraySignature()
should just be removed entirely. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…and GetBuiltInTypeArraySignature()
is required; trying to remove it breaks unit tests. Which means my "repro app" isn't accurate on the lookup side. After updating it, arrays are still faster:
% dotnet run -c Release
Array Creation: 392ms
Dict Creation: 753ms
Array Lookup: 156ms
Dict Lookup: 753ms
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing the manual work involved to test, should we have a BenchmarkDotNet project in this repo?
It seems like it would be useful to commit your benchmark above, and we'd have the option to run it later. BDN also does appropriate warmup, runs out of process, does some math, that might generally make it better than a manual benchmark.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should probably have one, and it should probably be in tests/Java.Interop-PerformanceTests
(and/or I could "just" add the above app to Java.Interop-PerformanceTests
…).
for (int c = constructors.Count; c > 0; --c) { | ||
var parameters = constructors [c-1].GetParameters (); | ||
for (int i = 0; i < parameters.Length; ++i) { | ||
if (!candidateParameterTypes [i].Contains (parameters [i].ParameterType)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should candidateParameterTypes
be a HashSet
? So Contains()
can be faster?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, I'm concerned about app startup time overheads, and I thus believe/assert that HashSet/Dictionary/etc. creation and population will take much more time than an array, and because this array is (1) small, and (2) never changed, init time should be minimal. (But I should verify this.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I was confused above; this is from ManagedPeer.Construct()
, which isn't part of app startup, and no static data is in the picture.
Despite being confused, I'm think I'm more or less right, though: each element of candidateParameterTypes
will be a small list, likely no more than 5 elements. I believe that the overhead of a HashSet vs. a List at such small sizes is not warranted. Arrays are fast!
Should we run an XA test PR on this before merging? |
Right now, nothing; this codepath is only hit by "JavaInterop1" Java Callable Wrappers, not Xamarin.Android "XAJavaInterop1" Java Callable Wrappers. (The codepath is present in .NET Android; it's just that only our unit tests will actually exercise it in practice.) Rephrased: the only way to hit it is for Java code to call This is thus a more "exploratory quest" to answer the question: so what about |
Attempting to return an `int[]` from a `[JavaCallable]` results in invalid IL: % $HOME/.dotnet/tools/ilverify bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll \ --tokens --system-module System.Private.CoreLib \ -r 'bin/TestDebug-net7.0/*.dll' \ -r '/usr/local/share/dotnet/shared/Microsoft.NETCore.App/7.0.10/*.dll' [IL]: Error [StackUnderflow]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x0000002F] Stack underflow. The offending IL is due to: call JavaInt32Array.ValueMarshaler..ctor which doesn't make sense; one should use `newobj` as part of `.ctor` invocations. Find the problem in `CecilCompilerExpressionVisitor`, attempt to fix it, re-run, and: [IL]: Error [MethodAccess]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x0000002F] Method is not visible. [IL]: Error [StackUnexpected]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x00000052][found value '[Java.Interop]Java.Interop.JniValueMarshalerState'][expected address of '[Java.Interop]Java.Interop.JniValueMarshalerState'] Unexpected type on the stack. [IL]: Error [StackUnexpected]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x00000057][found value '[Java.Interop]Java.Interop.JniObjectReference'][expected address of '[Java.Interop]Java.Interop.JniObjectReference'] Unexpected type on the stack. [IL]: Error [StackUnexpected]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x00000064][found value '[Java.Interop]Java.Interop.JniValueMarshalerState'][expected address of '[Java.Interop]Java.Interop.JniValueMarshalerState'] Unexpected type on the stack. [IL]: Error [StackUnexpected]: […/bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll : .__<$>_jni_marshal_methods::n_GetA(native int, native int)][offset 0x000000D0][found value '[Java.Interop]Java.Interop.JniValueMarshalerState'][expected address of '[Java.Interop]Java.Interop.JniValueMarshalerState'] Unexpected type on the stack. …which is why `jnimarshalmethod-gen` is, at best, a "research project" ("toy"), and not an Actual Product. (But `int[]` marshaling works *without* `jnimarshalmethod-gen`!) Avoid the problem, and return an `int` instead of an `int[]`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing that we tested out the performance, I don't really have any other concerns here. 👍
var isKeyProp = attr.Properties.FirstOrDefault (p => p.Name == "IsKeyword"); | ||
var isKeyword = isKeyProp.Name != null && ((bool) isKeyProp.Argument.Value) == true; | ||
var arrRankProp = attr.Properties.FirstOrDefault (p => p.Name == "ArrayRank"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the C# attribute available where we could use nameof()
? The resulting IL would be the same, but might help typos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this assembly doesn't have a reference to Java.Interop.dll
.
Context: dotnet/java-interop#1168 Does It Build™?
…Peer-il2057 Sanity check: PublicApiAnalyzers!
Context: #1168 (comment) Why arrays and not dictionaries, especially considering that 7079166 involves an O(N\*\*2) lookup? For two reasons: 1. the `JniPrimitiveArrayTypes` array is created for *all* apps, and thus contributes to app startup overheads for all apps. Thus, we want to minimize creation time. 2. the data is *small*: 8 entries within `JniPrimitiveArrayTypes`, each of which contains an array of 4 elements. *N* is *small* within O(N\*\*2). While this works for a gut feeling, time for some performance tests! Update `Java.Interop-PerformanceTests` to measure the timing overhead to create 1000000 instances of `JniPrimitiveArrayInfo[]` vs. the time to create 1000000 instances of `Dictionary<Type, JniPrimitiveArrayInfo>`. % dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop-PerformanceTests.dll … # CreationTiming: Array Creation: 403ms # CreationTiming: Dict Creation: 1483ms … # LookupTiming: Array Lookup: 97ms # LookupTiming: Dict Lookup: 173ms Array creation is pretty fast compared to a Dictionary, with Array creation taking 27% the time of Dictionaries. Lookup is also pretty fast, verifying that when *N* is small, O(N\*\*2) isn't much to worry about; lookup takes half the time. Note that this is for a *Debug* build; Release builds should make things better for Arrays.
@jpobst, @jonathanpeppers: @Suchiman on https://libera.chat / #csharp mentioned an alternate way to make
For example: using System.Text;
TellNativeAotAboutTypes ();
var t = Type.GetType (GetTypeName (), throwOnError: true);
Console.WriteLine (t);
string GetTypeName() =>
new StringBuilder()
.Append ("System.")
.Append ("Int32")
.Append (", System.Runtime")
.ToString ();
void TellNativeAotAboutTypes (bool load = false)
{
if (!load)
return;
Type.GetType ("System.Int32, System.Runtime");
} The above app works at runtime. If you remove the
However, compilation still generates an IL2057 warning:
So the good news is that we have a more plausible workaround in Java.Interop that doesn't require changing The bad news is that this described workaround means we have IL2057 trim warnings, but we could @jonathanpeppers, @jpobst: what do you think we should do? Revert #1168 so that |
Instead of Type GetTheType(TypeId id) =>
id switch
{
Id.Int32 => typeof(int),
// or Id.Int32 => Type.GetType("System.Int32, System.Runtime"),
} This way you don't have to do any suppressions, it's trim safe by construction. Alternatively: var dict = new Dictionary<string, Type>
{
{ "System.Int32, System.Runtime", typeof(int) },
...
} I assume you're going to do something with the type (like Activator.CreateInstance it), so you'll probably actually need something along the lines of this:
This way you can pass anything that comes out of this dictionary to |
(The perils of comments after closing the PR. I didn't see @MichalStrehovsky 's comment until just now…) @MichalStrehovsky: I lack the imagination to see how replacing Firstly, there's architecture: "core" assemblies like Secondly, generated Java code needs to be able to refer to C# types, and this mapping is done at packaging time. With enough hand-waving one can imagine a world where at app packaging time we create an int::type mapping, hard-coding these int values into the Java code. Thirdly, this isn't incremental: change an assembly, add a new type, and all your previous int::type mappings are potentially wrong. Rebuilding the world is slow, and something we need to avoid in Debug builds. This in turn might be solvable, but it doesn't look easy in the 5s of mental energy I gave it. It also doesn't quite consider the sheer number of entries we're looking at. The The The approach committed in 005c914 (which may or may not need to be reverted) builds atop the typemap infrastructure which already exists, and needs to exist, and has been optimized a fair bit. The alternate approach has the benefit of needing fewer changes overall, and avoids the need for me to construct a gigantic multi-thousand entry dict mapping strings to types. (Native AOT will presumably need to create such a mapping, somehow, but that's Not My Problem™ {until it is, due to perf hits}.) |
Context: dotnet/android#8543 Context: dotnet/android#8625 Context: #1168 Context: def5bc0 Context: 005c914 dotnet/android#8543 tested PR #1168, was Totally Green™ -- finding no issues -- and so we merged PR #1168 into 005c914. Enter dotnet/android#8625, which bumps xamarin-android to use def5bc0, which includes 005c914. dotnet/android#8625 contains *failing unit tests* (?!), including `Java.InteropTests.InvokeVirtualFromConstructorTests()`: Java.Lang.LinkageError : net.dot.jni.test.CallVirtualFromConstructorDerived ----> System.NotSupportedException : Could not find System.Type corresponding to Java type JniTypeSignature(TypeName=net/dot/jni/test/CallVirtualFromConstructorDerived ArrayRank=0 Keyword=False) . at Java.Interop.JniEnvironment.StaticMethods.GetStaticMethodID(JniObjectReference , String , String ) at Java.Interop.JniType.GetStaticMethod(String , String ) at Java.Interop.JniPeerMembers.JniStaticMethods.GetMethodInfo(String , String ) at Java.Interop.JniPeerMembers.JniStaticMethods.GetMethodInfo(String ) at Java.Interop.JniPeerMembers.JniStaticMethods.InvokeObjectMethod(String , JniArgumentValue* ) at Java.InteropTests.CallVirtualFromConstructorDerived.NewInstance(Int32 value) at Java.InteropTests.InvokeVirtualFromConstructorTests.ActivationConstructor() at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags ) --- End of managed Java.Lang.LinkageError stack trace --- java.lang.NoClassDefFoundError: net.dot.jni.test.CallVirtualFromConstructorDerived at crc643df67da7b13bb6b1.TestInstrumentation_1.n_onStart(Native Method) at crc643df67da7b13bb6b1.TestInstrumentation_1.onStart(TestInstrumentation_1.java:35) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189) Caused by: android.runtime.JavaProxyThrowable: [System.NotSupportedException]: Could not find System.Type corresponding to Java type JniTypeSignature(TypeName=net/dot/jni/test/CallVirtualFromConstructorDerived ArrayRank=0 Keyword=False) . at Java.Interop.ManagedPeer.GetTypeFromSignature(Unknown Source:0) at Java.Interop.ManagedPeer.RegisterNativeMembers(Unknown Source:0) at net.dot.jni.ManagedPeer.registerNativeMembers(Native Method) at net.dot.jni.test.CallVirtualFromConstructorDerived.<clinit>(CallVirtualFromConstructorDerived.java:12) ... 3 more --NotSupportedException at Java.Interop.ManagedPeer.GetTypeFromSignature(JniTypeManager , JniTypeSignature , String ) at Java.Interop.ManagedPeer.RegisterNativeMembers(IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, IntPtr n_methods) :shocked-pikachu-face: (But dotnet/android#8543 was green!) The problem is twofold: 1. 005c914 now requires the presence of typemap entries from e.g. `Java.InteropTests.CallVirtualFromConstructorDerived` to `net.dot.jni.test.CallVirtualFromConstructorDerived`. 2. `Java.Interop.Tools.JavaCallableWrappers` et al doesn't create typemap entries for `Java.Interop.JavaObject` subclasses which have `[JniTypeSignature]`. Consequently, our units tests fail (and apparently weren't *run* on dotnet/android#8543?! Still not what happened.) Fix typemap generation by adding a new `TypeDefinition.HasJavaPeer()` extension method to replace all the `.IsSubclassOf("Java.Lang.Object")` and similar checks, extending it to also check for `Java.Interop.JavaObject` and `Java.Interop.JavaException` base types. (Continuing to use base type checks is done instead of just relying on implementation of `Java.Interop.IJavaPeerable` as a performance optimization, as there could be *lots* of interface types to check.) Additionally, @jonathanpeppers -- while trying to investigate all this -- ran across a build failure: obj\Debug\net9.0-android\android\src\java\lang\Object.java(7,15): javac.exe error JAVAC0000: error: cyclic inheritance involving Object This suggests that `Java.Interop.Tools.JavaCallableWrappers` was encountering `Java.Interop.JavaObject` -- or some other type which has `[JniTypeSignature("java/lang/Object")]` -- which is why `java/lang/Object.java` was being generated. Audit all `[JniTypeSignature]` attributes, and add `GenerateJavaPeer=false` to all types which should *not* hava a Java Callable Wrapper generated for them. This includes nearly everything within `Java.Interop-Tests.dll`. (We want the typemaps! We *don't* want generated Java source, as we have hand-written Java peer types for those tests.) --- Aside: this project includes [T4 Text Templates][0]. To regenerate the output files *without involving Visual Studio*, you can install the [`dotnet-t4`][1] tool: $ dotnet tool install --global dotnet-t4 then run it separately for each `.tt` file: $HOME/.dotnet/tools/t4 -o src/Java.Interop/Java.Interop/JavaPrimitiveArrays.cs \ src/Java.Interop/Java.Interop/JavaPrimitiveArrays.tt [0]: https://learn.microsoft.com/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2022 [1]: https://www.nuget.org/packages/dotnet-t4/
…1181) Context: dotnet/android#8543 Context: dotnet/android#8625 Context: dotnet/android#8681 Context: #1168 Context: def5bc0 Context: 005c914 dotnet/android#8543 tested PR #1168, was Totally Green™ -- finding no issues -- and so we merged PR #1168 into 005c914. Enter dotnet/android#8625, which bumps xamarin-android to use def5bc0, which includes 005c914. dotnet/android#8625 contains *failing unit tests* (?!), including `Java.InteropTests.InvokeVirtualFromConstructorTests()`: Java.Lang.LinkageError : net.dot.jni.test.CallVirtualFromConstructorDerived ----> System.NotSupportedException : Could not find System.Type corresponding to Java type JniTypeSignature(TypeName=net/dot/jni/test/CallVirtualFromConstructorDerived ArrayRank=0 Keyword=False) . at Java.Interop.JniEnvironment.StaticMethods.GetStaticMethodID(JniObjectReference , String , String ) at Java.Interop.JniType.GetStaticMethod(String , String ) at Java.Interop.JniPeerMembers.JniStaticMethods.GetMethodInfo(String , String ) at Java.Interop.JniPeerMembers.JniStaticMethods.GetMethodInfo(String ) at Java.Interop.JniPeerMembers.JniStaticMethods.InvokeObjectMethod(String , JniArgumentValue* ) at Java.InteropTests.CallVirtualFromConstructorDerived.NewInstance(Int32 value) at Java.InteropTests.InvokeVirtualFromConstructorTests.ActivationConstructor() at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags ) --- End of managed Java.Lang.LinkageError stack trace --- java.lang.NoClassDefFoundError: net.dot.jni.test.CallVirtualFromConstructorDerived at crc643df67da7b13bb6b1.TestInstrumentation_1.n_onStart(Native Method) at crc643df67da7b13bb6b1.TestInstrumentation_1.onStart(TestInstrumentation_1.java:35) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189) Caused by: android.runtime.JavaProxyThrowable: [System.NotSupportedException]: Could not find System.Type corresponding to Java type JniTypeSignature(TypeName=net/dot/jni/test/CallVirtualFromConstructorDerived ArrayRank=0 Keyword=False) . at Java.Interop.ManagedPeer.GetTypeFromSignature(Unknown Source:0) at Java.Interop.ManagedPeer.RegisterNativeMembers(Unknown Source:0) at net.dot.jni.ManagedPeer.registerNativeMembers(Native Method) at net.dot.jni.test.CallVirtualFromConstructorDerived.<clinit>(CallVirtualFromConstructorDerived.java:12) ... 3 more --NotSupportedException at Java.Interop.ManagedPeer.GetTypeFromSignature(JniTypeManager , JniTypeSignature , String ) at Java.Interop.ManagedPeer.RegisterNativeMembers(IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, IntPtr n_methods) :shocked-pikachu-face: (But dotnet/android#8543 was green!) The problem is twofold: 1. 005c914 now requires the presence of typemap entries from e.g. `net.dot.jni.test.CallVirtualFromConstructorDerived` to `Java.InteropTests.CallVirtualFromConstructorDerived`. 2. `Java.Interop.Tools.JavaCallableWrappers` et al doesn't create typemap entries for `Java.Interop.JavaObject` subclasses which have `[JniTypeSignature]`. Consequently, our units tests fail (and apparently weren't *run* on dotnet/android#8543?! Still not sure what happened.) Update typemap generation by adding a new `TypeDefinition.HasJavaPeer()` extension method to replace all the `.IsSubclassOf("Java.Lang.Object")` and similar checks, extending it to also check for `Java.Interop.JavaObject` and `Java.Interop.JavaException` base types. (Continuing to use base type checks is done instead of just relying on implementation of `Java.Interop.IJavaPeerable` as a performance optimization, as there could be *lots* of interface types to check.) Additionally, @jonathanpeppers -- while trying to investigate all this -- ran across a build failure: obj\Debug\net9.0-android\android\src\java\lang\Object.java(7,15): javac.exe error JAVAC0000: error: cyclic inheritance involving Object This suggests that `Java.Interop.Tools.JavaCallableWrappers` was encountering `Java.Interop.JavaObject` -- or some other type which has `[JniTypeSignature("java/lang/Object")]` -- which is why `java/lang/Object.java` was being generated. Audit all `[JniTypeSignature]` attributes, and add `GenerateJavaPeer=false` to all types which should *not* hava a Java Callable Wrapper generated for them. This includes nearly everything within `Java.Interop-Tests.dll`. (We want the typemaps! We *don't* want generated Java source, as we have hand-written Java peer types for those tests.) Add `[JniTypeSignature]` to `GenericHolder<T>`. This type mapping isn't *actually* required, but it *is* used in `JavaVMFixture`, and it confuses people (me!) if things are inconsistent. Additionally, remove `tests/` from the Java-side name, for consistency. ~~ Avoid multiple java/lang/Object bindings ~~ A funny thing happened when in dotnet/android#8681 -- which tested this commit -- when using an intermediate version of this commit: unit tests started crashing! E monodroid-assembly: typemap: unable to load assembly 'Java.Interop-Tests' when looking up managed type corresponding to Java type 'java/lang/Object' What appears to be happening is an Unfortunate Interaction™: 1. `Java.Interop-Tests.dll` contained *multiple bindings* for `java/lang/Object`. e.g. [JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)] partial class JavaDisposedObject : JavaObject { } 2. The typemap generator has no functionality to "prioritize" one binding vs. another; it's random. As such, there is nothing to cause `Java.Lang.Object, Mono.Android` to be used as the preferred binding for `java/lang/Object`. This meant that when we hit the typemap codepath in .NET Android, we looked for the C# type that corresponded to `java/lang/Object`, found *some random type* from `Java.Interop-Tests`, and… …and then we hit another oddity: that codepath only supported looking for C# types in assemblies which had already been loaded. This was occurring during startup, so `Java.Interop-Tests` had not yet been loaded yet, so it errored out, returned `nullptr`, and later Android just aborts things: F droid.NET_Test: runtime.cc:638] JNI DETECTED ERROR IN APPLICATION: use of deleted local reference 0x79 Just…eep! This didn't happen before because `Java.Interop.JavaObject` subclasses *didn't* participate in typemap generation. This commit *adds* that support, introducing this unforeseen interaction. Fix this by *removing* most "alternate bindings" for `java/lang/Object`: - [JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)] + [JniTypeSignature (JniTypeName)] partial class JavaDisposedObject : JavaObject { + internal const string JniTypeName = "net/dot/jni/test/JavaDisposedObject"; } This implicitly requires that we now have a Java Callable Wrapper for this type, so update `Java.Interop-Tests.csproj` to run `jcw-gen` as part of the build process. This ensures that we create the JCW for e.g. `JavaDisposedObject`. Update `JavaVMFixture` to add the required typemap entries. --- Aside: this project includes [T4 Text Templates][0]. To regenerate the output files *without involving Visual Studio*, you can install the [`dotnet-t4`][1] tool: $ dotnet tool install --global dotnet-t4 then run it separately for each `.tt` file: $HOME/.dotnet/tools/t4 -o src/Java.Interop/Java.Interop/JavaPrimitiveArrays.cs \ src/Java.Interop/Java.Interop/JavaPrimitiveArrays.tt [0]: https://learn.microsoft.com/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2022 [1]: https://www.nuget.org/packages/dotnet-t4/
Context: dotnet/java-interop#1165 Context: dotnet/java-interop@005c914 Context: #8543 Context: dotnet/java-interop@07c7300 Context: #8625 Context: xamarin/monodroid@e3e4f12 Context: xamarin/monodroid@a04b73b Context: efbec22 Changes: dotnet/java-interop@8b85462...07c7300 * dotnet/java-interop@07c73009: [Java.Interop] Typemap support for JavaObject & `[JniTypeSignature]` (dotnet/java-interop#1181) * dotnet/java-interop@d529f3be: Bump to xamarin/xamarin-android-tools/main@ed102fc (dotnet/java-interop#1182) * dotnet/java-interop@def5bc0d: [ci] Add API Scan job (dotnet/java-interop#1178) * dotnet/java-interop@d5afa0af: [invocation-overhead] Add generated source files (dotnet/java-interop#1175) * dotnet/java-interop@473ef74c: Bump to xamarin/xamarin-android-tools/main@4889bf0 (dotnet/java-interop#1172) * dotnet/java-interop@005c9141: [Java.Interop] Avoid `Type.GetType()` in `ManagedPeer` (dotnet/java-interop#1168) * dotnet/java-interop@0f1efebd: [Java.Interop] Use PublicApiAnalyzers to ensure we do not break API (dotnet/java-interop#1170) (From the "infinite scream" department…) It started with a desire to remove some linker warnings (dotnet/java-interop#1165): external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs(93,19,93,112): warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type. dotnet/java-interop@005c9141 attempted to fix this by requiring the use of "typemaps" mapping Java type signatures to managed types, replacing e.g.: Type type = Type.GetType ("Example.Type, AssemblyName", throwOnError: true)!; Type[] parameterTypes = GetParameterTypes ("System.Int32:System.Int32"); ConstructorInfo ctor = type.GetConstructor (ptypes); // ctor=Example.Type(int, int) constructor with (not exactly, but for expository purposes): Type type = GetTypeFromSignature("crc64…/Type"); Type[] parameterTypes = GetConstructorCandidateParameterTypes ("(II)V"); ConstructorInfo ctor = type.GetConstructor (ptypes); // ctor=Example.Type(int, int) constructor among other changes. This was a *significant* change that would alter *Java.Interop* semantics but *not* .NET Android semantics -- .NET Android uses `Java.Interop.TypeManager.n_Activate()` (in this repo) for Java-side "activation" scenarios, not `Java.Interop.ManagedPeer` -- so in an abundance of caution we did a manual integration test in #8543 to make sure nothing broke before merging it. Something was apparently "off" in that integration. (We're still not sure what was off, or why it was completely green.) Ever since dotnet/java-interop@005c9141 was merged, every attempt to bump xamarin/Java.Interop has failed, in a number of ways described below. However, instead of reverting dotnet/java-interop@005c9141 we took this as an opportunity to understand *how and why* things were failing, as apparently we had encountered some *long-standing* corner cases in How Things Work. The oversights and failures include: 1. In order to make the Java.Interop unit tests work in .NET Android, the (largely hand-written) Java.Interop test types *also* need to participate with .NET Android typemap support, so that there is a typemap entry mapping `net/dot/jni/test/GenericHolder` to `Java.InteropTests.GenericHolder<T>` and vice-versa. dotnet/java-interop@07c73009 updates `Java.Interop.Tools.JavaCallableWrappers` to support creating typemap entries for `Java.Interop.JavaObject` subclasses, introducing a new `TypeDefinition.HasJavaPeer()` extension method. 2. (1) meant that, for the first time ever, types in `Java.Interop-Tests` participated in .NET Android type mapping. This *sounds* fine, except that `Java.Interop-Tests` contains "competing bindings" for `java.lang.Object`: [JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)] partial class JavaLangRemappingTestObject : JavaObject { } 3. (2) means that, for the first time ever, we *could* have the typemap entry for `java/lang/Object` map to `Java.InteropTests.JavaLangRemappingTestObject, Java.Interop-Tests`, *not* `Java.Lang.Object, Mono.Android`. Arguably a bug, arguably "meh", but this setup triggered some never previously encountered error conditions: 4. `EmbeddedAssemblies::typemap_java_to_managed()` within `libmonodroid.so` returns a `System.Type` that corresponds to a JNI type. `typemap_java_to_managed()` has a bug/corner case wherein it will only provide `Type` instances from assemblies which have already been loaded. Early in startup, `Java.Interop-Tests` hasn't been loaded yet, so when `java/lang/Object` was mapped to `Java.InteropTests.JavaLangRemappingTestObject, Java.Interop-Tests`, `typemap_java_to_managed()` would return `null`. This is a bug/corner case, which is being investigated in #8625. 5. Calls to `Java.Lang.Object.GetObject<T>()` call `Java.Interop.TypeManager.CreateInstance()`, which loops through the type and all base types to find a known binding/wrapper. Because of (3)+(4), if (when) we try to find the wrapper for `java/lang/Object`, we would find *no* mapping. This would cause an `JNI DETECTED ERROR IN APPLICATION` *crash*. This was due to a "use after free" bug. See the "TypeManager.CreateInstance() Use After Free Bug" section. 6. Once (5) is fixed we encounter our next issue: the `Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows()` unit test started failing because `crc641855b07eca6dcc03.GenericHolder_1` couldn't be found. This was caused by a bug in `acw-map.txt` parsing within `<R8/>`. See the "`<R8/>` and `acw-map.txt` parsing.`" section. 7. Once (6) was fixed, (3) caused a *new* set of failures: multiple tests started failing because `java/lang/Object` was being mapped to the wrong managed type. (3) becomes less "meh" and more "definitely a bug". See the "Correct `java/lang/Object` mappings" section. *Now* things should work reliably. ~~ TypeManager.CreateInstance() Use After Free Bug ~~ On 2011-Oct-19, xamarin/monodroid@e3e4f123d8 introduced a use-after-free bug within `TypeManager.CreateInstance()`: JNIEnv.DeleteRef (handle, transfer); throw new NotSupportedException ( FormattableString.Invariant ($"Internal error finding wrapper class for '{JNIEnv.GetClassNameFromInstance (handle)}'. (Where is the Java.Lang.Object wrapper?!)"), CreateJavaLocationException ()); `handle` *cannot be used* after `JNIEnv.DeleteRef(handle)`. Failure to do so results in a `JNI DETECTED ERROR IN APPLICATION` crash; with `adb shell setprop debug.mono.log lref+` set, we see: I monodroid-lref: +l+ lrefc 1 handle 0x71/L from thread '(null)'(1) D monodroid-gref: at Android.Runtime.AndroidObjectReferenceManager.CreatedLocalReference(JniObjectReference , Int32& ) D monodroid-gref: at Java.Interop.JniRuntime.JniObjectReferenceManager.CreatedLocalReference(JniEnvironmentInfo , JniObjectReference ) D monodroid-gref: at Java.Interop.JniEnvironment.LogCreateLocalRef(JniObjectReference ) D monodroid-gref: at Java.Interop.JniEnvironment.LogCreateLocalRef(IntPtr ) D monodroid-gref: at Java.Interop.JniEnvironment.InstanceMethods.CallObjectMethod(JniObjectReference , JniMethodInfo ) D monodroid-gref: … … I monodroid-lref: -l- lrefc 0 handle 0x71/L from thread '(null)'(1) D monodroid-gref: at Android.Runtime.AndroidObjectReferenceManager.DeleteLocalReference(JniObjectReference& , Int32& ) D monodroid-gref: at Java.Interop.JniRuntime.JniObjectReferenceManager.DeleteLocalReference(JniEnvironmentInfo , JniObjectReference& ) D monodroid-gref: at Java.Interop.JniObjectReference.Dispose(JniObjectReference& reference) D monodroid-gref: at Android.Runtime.JNIEnv.DeleteLocalRef(IntPtr ) D monodroid-gref: at Android.Runtime.JNIEnv.DeleteRef(IntPtr , JniHandleOwnership ) D monodroid-gref: at Java.Interop.TypeManager.CreateInstance(IntPtr , JniHandleOwnership , Type ) D monodroid-gref: at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) D monodroid-gref: at Java.Lang.Object._GetObject[IIterator](IntPtr , JniHandleOwnership ) D monodroid-gref: at Java.Lang.Object.GetObject[IIterator](IntPtr handle, JniHandleOwnership transfer) D monodroid-gref: … D monodroid-gref: E droid.NET_Test: JNI ERROR (app bug): accessed stale Local 0x71 (index 7 in a table of size 7) F droid.NET_Test: java_vm_ext.cc:570] JNI DETECTED ERROR IN APPLICATION: use of deleted local reference 0x71 … F droid.NET_Test: runtime.cc:630] native: #13 pc 00000000003ce865 /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::CheckJNI::GetObjectClass(_JNIEnv*, _jobject*)+837) The immediate fix is Don't Do That™; use a temporary: class_name = JNIEnv.GetClassNameFromInstance (handle); JNIEnv.DeleteRef (handle, transfer); throw new NotSupportedException ( FormattableString.Invariant ($"Internal error finding wrapper class for '{class_name}'. (Where is the Java.Lang.Object wrapper?!)"), CreateJavaLocationException ()); Unfortunately, *just* fixing the "use-after-free" bug is insufficient; if we throw that `NotSupportedException`, things *will* break elsewhere. We'll just have an "elegant unhandled exception" app crash instead of a "THE WORLD IS ENDING" failed assertion crash. We could go with the simple fix for the crash, but this means that in order to integrate dotnet/java-interop@005c9141 & dotnet/java-interop@07c73009 we'd have to figure out how to *ensure* that `java/lang/Object` is bound as `Java.Lang.Object, Mono.Android`, not `Java.InteropTests.JavaLangRemappingTestObject, Java.Interop-Tests`. (We actually need to do this *anyway*; see the "Correct `java/lang/Object` mappings" section. At the time we I was trying to *avoid* special-casing `Mono.Android.dll`…) There is a*slightly* more complicated approach which fixes (5) while supporting (4) `typemap_java_to_managed()` returning null; consider the `-l-` callstack: at Android.Runtime.JNIEnv.DeleteRef(IntPtr , JniHandleOwnership ) at Java.Interop.TypeManager.CreateInstance(IntPtr , JniHandleOwnership , Type ) at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Java.Lang.Object._GetObject[IIterator](IntPtr , JniHandleOwnership ) at Java.Lang.Object.GetObject[IIterator](IntPtr handle, JniHandleOwnership transfer) at Android.Runtime.JavaSet.Iterator() This is part of a generic `Object.GetObject<IIterator>()` invocation! Additionally, because `IIterator` is an interface, in *normal* use the `type` variable within `TypeManager.CreateInstance()` would be `Java.Lang.Object, Mono.Android` and then *immediately discarded* because `Java.Lang.Object` cannot be assigned to `IIterator`. Moving the type compatibility check to *before* the `type == null` check fixes *an* issue with `typemap_java_to_managed()` returning null. ~~ `<R8/>` and `acw-map.txt` parsing.` ~~ There are many ways for Android+Java code to refer to managed types. For example, consider the following View subclass: namespace Example { partial class MyCoolView : Android.Views.View { // … } } Within layout `.axml` files, you can mention an `Android.Views.View` subclass by: * Using the .NET Full Class Name as an element name. <Example.MyCoolView /> * Using the .NET Full Class Name with a *lowercased* namespace name as the element name. <example.MyCoolView /> * Use the Java-side name directly. <crc64….NiftyView /> Within Fragments, you can also use the *assembly-qualified name*: <fragment class="Example.MyCoolView, AssemblyName" /> At build time, all instances of the .NET type names will be *replaced* with the Java type names before the Android toolchain processes the files. The association between .NET type names and Java names is stored within `$(IntermediateOutputPath)acw-map.txt`, which was introduced in xamarin/monodroid@a04b73b3. *Normally* `acw-map.txt` contains three entries: 1. The fully-qualified .NET type name 2. The .NET type name, no assembly 3. (2) with a lowercased namespace name, *or* the `[Register]` value, if provided. For example: Mono.Android_Test.Library.CustomTextView, Mono.Android-Test.Library.NET;crc6456ab8145c81c4100.CustomTextView Mono.Android_Test.Library.CustomTextView;crc6456ab8145c81c4100.CustomTextView mono.android_test.library.CustomTextView;crc6456ab8145c81c4100.CustomTextView Java.InteropTests.GenericHolder`1, Java.Interop-Tests;net.dot.jni.test.tests.GenericHolder Java.InteropTests.GenericHolder`1;net.dot.jni.test.tests.GenericHolder net.dot.jni.test.tests.GenericHolder;net.dot.jni.test.tests.GenericHolder However, when warning XA4214 is emitted (efbec22), there is a "collision" on the .NET side (but *not* the Java side); (2) and (3) are potentially *ambiguous*, so one .NET type is arbitrarily chosen. (Collisions on the Java side result in XA4215 *errors*.) The first line is still possible, because of assembly qualification. Enter ``Java.InteropTests.GenericHolder`1``: this type is present in *both* `Java.Interop-Tests.dll` *and* `Mono.Android-Tests.dll`. dotnet/java-interop@07c73009, this was "fine" because the `GenericHolder<T>` within `Java.Interop-Tests.dll` did not participate in typemap generation. Now it does, resulting in the XA4214 warning. XA4214 *also* means that instead of three lines, it's *one* line: Java.InteropTests.GenericHolder`1, Mono.Android.NET-Tests;crc641855b07eca6dcc03.GenericHolder_1 Enter `<R8/>`, which parses `acw-map.txt` to create a `proguard_project_primary.cfg` file. `<R8/>` did it's *own* parsing of `acw-map.txt`, parsing only *one of every three lines*, on the assumption that *all* entries took three lines. This breaks in the presence of XA4214, because some entries only take one line, not three lines. This in turn meant that `proguard_project_primary.cfg` could *miss* types, which could mean that `r8` would *remove* the unspecified types, resulting in `ClassNotFoundException` at runtime: Java.Lang.ClassNotFoundException : crc641855b07eca6dcc03.GenericHolder_1 ----> Java.Lang.ClassNotFoundException : Didn't find class "crc641855b07eca6dcc03.GenericHolder_1" on path: DexPathList[[zip file "/data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/base.apk", zip file "/data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/split_config.x86_64.apk", zip file "/data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/split_config.xxhdpi.apk"],nativeLibraryDirectories=[/data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/lib/x86_64, /system/fake-libs64, /data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/base.apk!/lib/x86_64, /data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/split_config.x86_64.apk!/lib/x86_64, /data/app/Mono.Android.NET_Tests-2stBqO43ov5F6bHfYemJHQ==/split_config.xxhdpi.apk!/lib/x86_64, /system/lib64, /system/product/lib64]] at Java.Interop.JniEnvironment.StaticMethods.CallStaticObjectMethod(JniObjectReference , JniMethodInfo , JniArgumentValue* ) at Android.Runtime.JNIEnv.FindClass(String ) Update `<R8/>` to instead use `MonoAndroidHelper.LoadMapFile()`, which reads all lines within `acw-map.txt`. This results in a `proguard_project_primary.cfg` file which properly contains a `-keep` entry for XA4214-related types, such as `crc641855b07eca6dcc03.GenericHolder_1`. ~~ Correct `java/lang/Object` mappings ~~` Previous valiant efforts to allow `java/lang/Object` to be mapped to "anything", not just `Java.Lang.Object, Mono.Android`, eventually resulted in lots of unit test failures, e.g.: `Android.RuntimeTests.XmlReaderPullParserTest.ToLocalJniHandle()`: System.NotSupportedException : Unable to activate instance of type Java.InteropTests.JavaLangRemappingTestObject from native handle 0x19 (key_handle 0x2408476). ----> System.MissingMethodException : No constructor found for Java.InteropTests.JavaLangRemappingTestObject::.ctor(System.IntPtr, Android.Runtime.JniHandleOwnership) ----> Java.Interop.JavaLocationException : Exception_WasThrown, Java.Interop.JavaLocationException at Java.Interop.TypeManager.CreateInstance(IntPtr , JniHandleOwnership , Type ) at Java.Interop.TypeManager.CreateInstance(IntPtr , JniHandleOwnership ) at Android.Runtime.XmlResourceParserReader.FromNative(IntPtr , JniHandleOwnership ) at Android.Runtime.XmlResourceParserReader.FromJniHandle(IntPtr handle, JniHandleOwnership transfer) at Android.Content.Res.Resources.GetXml(Int32 ) at Android.RuntimeTests.XmlReaderPullParserTest.ToLocalJniHandle() at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags ) --MissingMethodException at Java.Interop.TypeManager.CreateProxy(Type , IntPtr , JniHandleOwnership ) at Java.Interop.TypeManager.CreateInstance(IntPtr , JniHandleOwnership , Type ) With a partially heavy heart, we need to special-case typemap entries by processing `Mono.Android.dll` *first*, so that it gets first dibs at bindings for `java/lang/Object` and other types. Update `NativeTypeMappingData` to process types from `Mono.Android` before processing any other module. Note that the special-casing needs to happen in `NativeTypeMappingData` because typemaps were formerly processed in *sorted module order*, in which the sort order is based on the *byte representation* of the module's MVID (a GUID). Additionally, *linking changes the MVID*, which means module order is *effectively random*. Consequently, trying to special case typemap ordering anywhere else is ineffective. ~~ Other ~~ Update `JavaCompileToolTask` to log the contents of its response file. Update LLVM-IR -related types within `src/Xamarin.Android.Build.Tasks/Utilities` to use `TaskLoggingHelper` for logging purposes, *not* `Action<string>`. Update related types to accept `TaskLoggingHelper`, so that we can more easily add diagnostic messages to these types in the future.
Fixes: #1165
Context: #1153
Context: #1157
When building for NativeAOT (#1153) or when building .NET Android apps with
-p:IsAotcompatible=true
(#1157), we get IL2057 warnings fromManagedPeer.cs
:These warnings are because
ManagedPeer.Construct()
andManagedPeer.RegisterNativeMembers()
useType.GetType()
on string values provided from Java code, and thus the IL trimmer does not have visibility into those strings, and thus cannot reliably determine which types need to be preserved:ManagedPeer.construct()
passes two sets of assembly-qualified type names:assemblyQualifiedName
contains the type to construct, whileconstructorSignature
contains a:
-separated list of assembly-qualified type names for the constructor parameters. Each of these are passed toType.GetType()
.ManagedPeer.registerNativeMembers()
passes an assembly-qualified type name toManagedPeer.RegisterNativeMembers()
, which passes the assembly-qualified type name toType.GetType()
to find the type to register native methods for.If we more strongly rely on JNI signatures, we can remove the need for Java Callable Wrappers to contain assembly-qualified type names entirely, thus removing the need for
ManagedPeer
to useType.GetType()
, removing the IL2057 warnings.For
ManagedPeer.construct()
,assemblyQualifiedName
can be replaced with getting the JNI type signature fromself.getClass()
, andconstructorSignature
can be replaced with aJNI method signature of the calling constructor.
For
ManagedPeer.registerNativeMembers()
,assemblyQualifiedName
can be replaced with getting the JNI type signature fromnativeClass
.Furthermore, if we add
[DynamicallyAccessedMembers]
toJniRuntime.JniTypeManager.GetType()
, we can fix some IL2075 warnings which appeared after fixing the IL2057 warnings.Aside: Excising assembly-qualified type names from Java Callable Wrappers had some "interesting" knock-on effects in the unit tests, requiring that more typemap information be explicitly provided. (This same information was implicitly provided before, via the provision of assembly-qualified type names everywhere…)