Skip to content

Commit 9dea87d

Browse files
authored
[Java.Interop] .GetTypeSignature() supports unsigned types (#1312)
Context: dotnet/android#9747 Context: dotnet/android#9812 Context: 71afce5 Context: dotnet/android@aa5e597 Context: dotnet/android@f800c1a In the ongoing epic to get MAUI running atop NativeAOT, we hit our longstanding NativeAOT nemesis: a P/Invoke: E AndroidRuntime: net.dot.jni.internal.JavaProxyThrowable: System.InvalidProgramException: InvalidProgram_Specific, IntPtr Android.Runtime.JNIEnv.monodroid_typemap_managed_to_java(System.Type, Byte*) E AndroidRuntime: at Internal.Runtime.TypeLoaderExceptionHelper.CreateInvalidProgramException(ExceptionStringID, String) + 0x4c E AndroidRuntime: at Android.Runtime.JNIEnv.monodroid_typemap_managed_to_java(Type, Byte*) + 0x18 E AndroidRuntime: at Android.Runtime.JNIEnv.TypemapManagedToJava(Type) + 0x104 E AndroidRuntime: at Android.Runtime.JNIEnv.GetJniName(Type) + 0x1c E AndroidRuntime: at Android.Runtime.JNIEnv.FindClass(Type) + 0x38 E AndroidRuntime: at Android.Runtime.JNIEnv.NewArray(IJavaObject[]) + 0x28 E AndroidRuntime: at Android.Runtime.JNIEnv.NewArray[T](T[]) + 0x94 E AndroidRuntime: at Android.Graphics.Drawables.LayerDrawable..ctor(Drawable[] layers) + 0xd4 E AndroidRuntime: at Microsoft.Maui.Platform.MauiRippleDrawableExtensions.UpdateMauiRippleDrawableBackground(View, Paint, IButtonStroke, Func`1, Func`1, Action) + 0x2ac (`JNIEnv.monodroid_typemap_managed_to_java()` is P/Invoke. Why are P/Invokes bad? See dotnet/android@f800c1a6.) The reasonable fix/workaround: update `JNIEnv.FindClass(Type)` to instead use `JniRuntime.JniTypeManager.GetTypeSignature(Type)`. (Also known as "embrace more JniRuntime abstractions!".) Unfortunately, this straightforward idea hits a minor schism between the .NET for Android and builtin java-interop world orders: How should Java `byte` be bound? For starters, what *is* a [Java `byte`][0]? > The values of the integral types are integers in the following ranges: > > * For `byte`, from -128 to 127, inclusive The Java `byte` is *signed*! Because of that, and because java-interop originated as a Second System Syndrome rebuild of Xamarin.Android, *of course* java-interop bound Java `byte` as `System.SByte`. .NET for Android, though, bound Java `byte` as `System.Byte`. This "minor" change meant that lots of unit tests started failing, e.g. [`NetworkInterfacesTest.DotNetInterfacesShouldEqualJavaInterfaces()`][2]: System.ArgumentException : Could not determine Java type corresponding to System.Byte[], System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e. Arg_ParamName_Name, type at Android.Runtime.JNIEnv.FindClass(Type ) at Android.Runtime.JNIEnv.AssertCompatibleArrayTypes(IntPtr , Type ) at Android.Runtime.JNIEnv._GetArray(IntPtr , Type ) at Android.Runtime.JNIEnv.GetArray(IntPtr , JniHandleOwnership , Type ) at Java.Net.NetworkInterface.GetHardwareAddress() at System.NetTests.NetworkInterfacesTest.GetInfos(IEnumeration interfaces) at System.NetTests.NetworkInterfacesTest.DotNetInterfacesShouldEqualJavaInterfaces() at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags ) Rephrased, `runtime.TypeManager.GetTypeSignature(typeof(byte[]))` returned a "default" `JniTypeSignature` instance. It's time to reduce the size of this schism. Update `JniBuiltinMarshalers.GetBuiltInTypeSignature()` so that `TypeCode.Byte` is treated as a synonym for `TypeCode.SByte`. This is in fact all that's needed in order to add support for `byte[]`! Repeat this exercise for all other unsigned types: `ushort`, `uint`, and `ulong`, as Kotlin unsigned types require it; see also 71afce5 and dotnet/android@aa5e597eba. This fixes the exception: System.InvalidCastException : Unable to cast from '[I' to '[Ljava/lang/Object;'. at Android.Runtime.JNIEnv.AssertCompatibleArrayTypes(IntPtr , Type ) at Android.Runtime.JNIEnv._GetArray(IntPtr , Type ) at Android.Runtime.JNIEnv.GetArray(IntPtr , JniHandleOwnership , Type ) at Foo.UnsignedInstanceMethods.UintArrayInstanceMethod(UInt32[] value) at Xamarin.Android.JcwGenTests.KotlinUnsignedTypesTests.TestUnsignedArrayTypeMembers() at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags ) This is *not* all that's necessary to fix all dotnet/android tests. Update `JniRuntime.JniTypeManager.GetTypeSignature()` and `.GetTypeSignatures()` so that if the type is an open generic type a `System.NotSupportedException` is thrown instead of a `System.ArgumentException`. This fixes [`Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows()`][3]. Also, `JniBuiltinMarshalers.cs` has some hand-made changes, rendering it out of sync with `JniBuiltinMarshalers.tt`. Update `JniBuiltinMarshalers.tt` appropriately and regenerate it. [0]: https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.2.1 [1]: https://github.com/dotnet/java-interop/blob/f30e420a1638dc013302e85dcf76642c10c26832/Documentation/Motivation.md [2]: https://github.com/dotnet/android/blob/1b1f1452f6b05707418d6605c06e106e6a2a6381/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/NetworkInterfaces.cs#L107-L137 [3]: https://github.com/dotnet/android/blob/1b1f1452f6b05707418d6605c06e106e6a2a6381/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs#L107-L116
1 parent 1cfb4f4 commit 9dea87d

File tree

4 files changed

+35
-22
lines changed

4 files changed

+35
-22
lines changed

src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,22 @@ static bool GetBuiltInTypeSignature (Type type, ref JniTypeSignature signature)
4949
case TypeCode.Boolean:
5050
signature = GetCachedTypeSignature (ref __BooleanTypeSignature, "Z", arrayRank: 0, keyword: true);
5151
return true;
52+
case TypeCode.Byte:
5253
case TypeCode.SByte:
5354
signature = GetCachedTypeSignature (ref __SByteTypeSignature, "B", arrayRank: 0, keyword: true);
5455
return true;
5556
case TypeCode.Char:
5657
signature = GetCachedTypeSignature (ref __CharTypeSignature, "C", arrayRank: 0, keyword: true);
5758
return true;
59+
case TypeCode.UInt16:
5860
case TypeCode.Int16:
5961
signature = GetCachedTypeSignature (ref __Int16TypeSignature, "S", arrayRank: 0, keyword: true);
6062
return true;
63+
case TypeCode.UInt32:
6164
case TypeCode.Int32:
6265
signature = GetCachedTypeSignature (ref __Int32TypeSignature, "I", arrayRank: 0, keyword: true);
6366
return true;
67+
case TypeCode.UInt64:
6468
case TypeCode.Int64:
6569
signature = GetCachedTypeSignature (ref __Int64TypeSignature, "J", arrayRank: 0, keyword: true);
6670
return true;
@@ -74,9 +78,6 @@ static bool GetBuiltInTypeSignature (Type type, ref JniTypeSignature signature)
7478
case TypeCode.DBNull:
7579
case TypeCode.Decimal:
7680
case TypeCode.Empty:
77-
case TypeCode.UInt16:
78-
case TypeCode.UInt32:
79-
case TypeCode.UInt64:
8081
return false;
8182
}
8283

src/Java.Interop/Java.Interop/JniBuiltinMarshalers.tt

+18-14
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ using Java.Interop.Expressions;
1818
namespace Java.Interop {
1919
<#
2020
var types = new[]{
21-
new { Name = "Boolean", Type = "Boolean", JniCallType = "Boolean", JniType = "Z", GetValue = "booleanValue" },
22-
new { Name = "Byte", Type = "SByte", JniCallType = "Byte", JniType = "B", GetValue = "byteValue" },
23-
new { Name = "Character", Type = "Char", JniCallType = "Char", JniType = "C", GetValue = "charValue" },
24-
new { Name = "Short", Type = "Int16", JniCallType = "Short", JniType = "S", GetValue = "shortValue" },
25-
new { Name = "Integer", Type = "Int32", JniCallType = "Int", JniType = "I", GetValue = "intValue" },
26-
new { Name = "Long", Type = "Int64", JniCallType = "Long", JniType = "J", GetValue = "longValue" },
27-
new { Name = "Float", Type = "Single", JniCallType = "Float", JniType = "F", GetValue = "floatValue" },
28-
new { Name = "Double", Type = "Double", JniCallType = "Double", JniType = "D", GetValue = "doubleValue" },
21+
new { Name = "Boolean", Type = "Boolean", UnsignedType = "", JniCallType = "Boolean", JniType = "Z", GetValue = "booleanValue" },
22+
new { Name = "Byte", Type = "SByte", UnsignedType = "Byte", JniCallType = "Byte", JniType = "B", GetValue = "byteValue" },
23+
new { Name = "Character", Type = "Char", UnsignedType = "", JniCallType = "Char", JniType = "C", GetValue = "charValue" },
24+
new { Name = "Short", Type = "Int16", UnsignedType = "UInt16", JniCallType = "Short", JniType = "S", GetValue = "shortValue" },
25+
new { Name = "Integer", Type = "Int32", UnsignedType = "UInt32", JniCallType = "Int", JniType = "I", GetValue = "intValue" },
26+
new { Name = "Long", Type = "Int64", UnsignedType = "UInt64", JniCallType = "Long", JniType = "J", GetValue = "longValue" },
27+
new { Name = "Float", Type = "Single", UnsignedType = "", JniCallType = "Float", JniType = "F", GetValue = "floatValue" },
28+
new { Name = "Double", Type = "Double", UnsignedType = "", JniCallType = "Double", JniType = "D", GetValue = "doubleValue" },
2929
};
3030
#>
3131

@@ -57,6 +57,11 @@ namespace Java.Interop {
5757
return true;
5858
<#
5959
foreach (var type in types) {
60+
if (!string.IsNullOrEmpty (type.UnsignedType)) {
61+
#>
62+
case TypeCode.<#= type.UnsignedType #>:
63+
<#
64+
}
6065
#>
6166
case TypeCode.<#= type.Type #>:
6267
signature = GetCachedTypeSignature (ref __<#= type.Type #>TypeSignature, "<#= type.JniType #>", arrayRank: 0, keyword: true);
@@ -68,9 +73,6 @@ namespace Java.Interop {
6873
case TypeCode.DBNull:
6974
case TypeCode.Decimal:
7075
case TypeCode.Empty:
71-
case TypeCode.UInt16:
72-
case TypeCode.UInt32:
73-
case TypeCode.UInt64:
7476
return false;
7577
}
7678

@@ -185,7 +187,7 @@ namespace Java.Interop {
185187
public override object? CreateValue (
186188
ref JniObjectReference reference,
187189
JniObjectReferenceOptions options,
188-
[DynamicallyAccessedMembers (ConstructorsAndInterfaces)]
190+
[DynamicallyAccessedMembers (Constructors)]
189191
Type? targetType)
190192
{
191193
if (!reference.IsValid)
@@ -196,7 +198,7 @@ namespace Java.Interop {
196198
public override <#= type.Type #> CreateGenericValue (
197199
ref JniObjectReference reference,
198200
JniObjectReferenceOptions options,
199-
[DynamicallyAccessedMembers (ConstructorsAndInterfaces)]
201+
[DynamicallyAccessedMembers (Constructors)]
200202
Type? targetType)
201203
{
202204
if (!reference.IsValid)
@@ -230,6 +232,7 @@ namespace Java.Interop {
230232
state = new JniValueMarshalerState ();
231233
}
232234

235+
[RequiresDynamicCode (ExpressionRequiresUnreferencedCode)]
233236
[RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]
234237
public override Expression CreateParameterToManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize, Type? targetType)
235238
{
@@ -242,6 +245,7 @@ namespace Java.Interop {
242245
return sourceValue;
243246
}
244247

248+
[RequiresDynamicCode (ExpressionRequiresUnreferencedCode)]
245249
[RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)]
246250
public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue)
247251
{
@@ -256,7 +260,7 @@ namespace Java.Interop {
256260
public override <#= type.Type #>? CreateGenericValue (
257261
ref JniObjectReference reference,
258262
JniObjectReferenceOptions options,
259-
[DynamicallyAccessedMembers (ConstructorsAndInterfaces)]
263+
[DynamicallyAccessedMembers (Constructors)]
260264
Type? targetType)
261265
{
262266
if (!reference.IsValid)

src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public JniTypeSignature GetTypeSignature (Type type)
146146
if (type == null)
147147
throw new ArgumentNullException (nameof (type));
148148
if (type.ContainsGenericParameters)
149-
throw new ArgumentException ($"'{type}' contains a generic type definition. This is not supported.", nameof (type));
149+
throw new NotSupportedException ($"'{type}' contains a generic type definition. This is not supported.");
150150

151151
type = GetUnderlyingType (type, out int rank);
152152

@@ -184,7 +184,7 @@ public IEnumerable<JniTypeSignature> GetTypeSignatures (Type type)
184184
if (type == null)
185185
yield break;
186186
if (type.ContainsGenericParameters)
187-
throw new ArgumentException ($"'{type}' contains a generic type definition. This is not supported.", nameof (type));
187+
throw new NotSupportedException ($"'{type}' contains a generic type definition. This is not supported.");
188188

189189
type = GetUnderlyingType (type, out int rank);
190190

tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs

+11-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void GetTypeSignature_Type ()
1818
Assert.Throws<ArgumentException>(() => JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (int[,])));
1919
Assert.Throws<ArgumentException>(() => JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (int[,][])));
2020
Assert.Throws<ArgumentException>(() => JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (int[][,])));
21-
Assert.Throws<ArgumentException>(() => JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (Action<>)));
21+
Assert.Throws<NotSupportedException>(() => JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (Action<>)));
2222
Assert.AreEqual (null, JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (typeof (JniRuntimeTest)).SimpleReference);
2323

2424
AssertGetJniTypeInfoForType (typeof (string), "java/lang/String", false, 0);
@@ -44,6 +44,12 @@ public void GetTypeSignature_Type ()
4444
AssertGetJniTypeInfoForType (typeof (int[][]), "[[I", true, 2);
4545
AssertGetJniTypeInfoForType (typeof (int[][][]), "[[[I", true, 3);
4646

47+
// We map unsigned types to their signed counterparts
48+
AssertGetJniTypeInfoForType (typeof (byte[]), "[B", true, 1);
49+
AssertGetJniTypeInfoForType (typeof (ushort[]), "[S", true, 1);
50+
AssertGetJniTypeInfoForType (typeof (uint[]), "[I", true, 1);
51+
AssertGetJniTypeInfoForType (typeof (ulong[]), "[J", true, 1);
52+
4753
AssertGetJniTypeInfoForType (typeof (JavaSByteArray), "[B", true, 1);
4854
AssertGetJniTypeInfoForType (typeof (JavaInt16Array), "[S", true, 1);
4955
AssertGetJniTypeInfoForType (typeof (JavaInt32Array), "[I", true, 1);
@@ -89,14 +95,16 @@ public void GetTypeSignature_Type ()
8995
static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeyword, int arrayRank)
9096
{
9197
var info = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (type);
98+
Assert.IsTrue (info.IsValid, $"info.IsValid for `{type}`");
9299

93100
// `GetTypeSignature() and `GetTypeSignatures()` should be "in sync"; verify that!
94101
var info2 = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignatures (type).FirstOrDefault ();
102+
Assert.IsTrue (info2.IsValid, $"info2.IsValid for `{type}`");
95103

96104
Assert.AreEqual (jniType, info.Name, $"info.Name for `{type}`");
97-
Assert.AreEqual (jniType, info2.Name, $"info.Name for `{type}`");
105+
Assert.AreEqual (jniType, info2.Name, $"info2.Name for `{type}`");
98106
Assert.AreEqual (arrayRank, info.ArrayRank, $"info.ArrayRank for `{type}`");
99-
Assert.AreEqual (arrayRank, info2.ArrayRank, $"info.ArrayRank for `{type}`");
107+
Assert.AreEqual (arrayRank, info2.ArrayRank, $"info2.ArrayRank for `{type}`");
100108
}
101109

102110
[Test]

0 commit comments

Comments
 (0)