Skip to content

Commit 7117414

Browse files
authored
[monodroid] Return of the Strings (#4487)
Fixes: #4415 Context: ce2bc68 Commit ce2bc68 optimized type mappings between managed types and Java types in large part by removing strings from the managed -> JNI mapping: instead of using an assembly-qualified *string* as a key, the assembly MVID & type metadata token were used as keys. This setup works reliably in Release apps, in which the assemblies don't change. In a commercial Debug with Fast Deployment situation, it falls down badly because every change to any source code that's built into a mapped assembly may cause the assembly to change its MVID, and renaming of any type -- removing or adding a type -- will rearrange the type definition table in the resulting assembly, thus changing the type token ids (which are basically offsets into the type definition table in the PE executable). This is what may cause an app to crash on the runtime with an exception similar to: android.runtime.JavaProxyThrowable: System.NotSupportedException: Cannot create instance of type 'com.glmsoftware.OBDNowProto.SettingsFragmentCompat': no Java peer type found. at Java.Interop.JniPeerMembers+JniInstanceMethods..ctor (System.Type declaringType) [0x0004b] in <e3e4dfa992a7411b85acfe193481be3e>:0 at Java.Interop.JniPeerMembers+JniInstanceMethods.GetConstructorsForType (System.Type declaringType) [0x00031] in <e3e4dfa992a7411b85acfe193481be3e>:0 at Java.Interop.JniPeerMembers+JniInstanceMethods.StartCreateInstance (System.String constructorSignature, System.Type declaringType, Java.Interop.JniArgumentValue* parameters) [0x00038] in <e3e4dfa992a7411b85acfe193481be3e>:0 at AndroidX.Preference.PreferenceFragmentCompat..ctor () [0x00034] in <005e3ae6340747e1aea6d08b095cf286>:0 at com.glmsoftware.OBDNowProto.SettingsFragmentCompat..ctor () [0x00026] in <a8dbee4be1674aa08cce57b50f21e347>:0 at com.glmsoftware.OBDNowProto.SettingsActivity.OnCreate (Android.OS.Bundle bundle) [0x00083] in <a8dbee4be1674aa08cce57b50f21e347>:0 at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_ (System.IntPtr jnienv, System.IntPtr native__this, System.IntPtr native_savedInstanceState) [0x00011] in <c56099afccf04721853684f376a89527>:0 at (wrapper dynamic-method) Android.Runtime.DynamicMethodNameCounter.3(intptr,intptr,intptr) at crc64596a13587a898911.SettingsActivity.n_onCreate(Native Method) at crc64596a13587a898911.SettingsActivity.onCreate(SettingsActivity.java:40) at android.app.Activity.performCreate(Activity.java:7825) at android.app.Activity.performCreate(Activity.java:7814) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1306) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) A "workaround" would be fully rebuild the application, which negates the point to incremental builds and the inner-dev-loop cycle. The fix is to partially revert ce2bc68 in the sense that it restores the use of string-based type names for Java-to-Managed and Managed-to-Java type lookups for ***Debug*** builds only. Unlike the pre-ce2bc689 world, only *one* copy of the set of string names is present within the data structures, at a tiny (sub millisecond) expense at the run time to fix up pointers between the two tables. ~~ File Formats ~~ All data in all file formats remains little-endian. In Debug configuration builds, each assembly will have a corresponding `*.typemap` file which will be loaded at runtime. The file format in pseudo-C++: struct DebugTypemapFileHeader { byte magic [4]; // "XATS" uint32_t format_version; // 2 uint32_t entry_count; uint32_t java_type_name_width; uint32_t managed_type_name_width; uint32_t assembly_name_size; byte assembly_name [assembly_name_size]; DebugTypemapFileJavaToManagedEntry java_to_managed [entry_count]; DebugTypemapFileManagedToJavaEntry managed_to_java [entry_count]; } struct DebugTypemapFileJavaToManagedEntry { byte jni_name [DebugTypemapFileHeader::java_type_name_width]; uint32_t managed_index; // Index into DebugTypemapFileHeader::managed_to_java }; struct DebugTypemapFileManagedToJavaEntry { byte managed_name [DebugTypemapFileHeader::java_type_name_width]; uint32_t jni_index; // Index into DebugTypemapFileHeader::java_to_managed }; `DebugTypemapFileHeader::java_type_name_width` and `DebugTypemapFileHeader::managed_type_name_width` are the maximum length + 1 (terminating NUL) for JNI names and assembly-qualified managed names. `DebugTypemapFileJavaToManagedEntry::jni_name` and `DebugTypemapFileManagedToJavaEntry::managed_name` are NUL-padded.
1 parent 15a800d commit 7117414

24 files changed

+804
-405
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#### Application behavior on device and emulator
2+
3+
* [GitHub 4415](https://github.com/xamarin/xamarin-android/issues/4415):
4+
Starting in Xamarin.Android 10.2.100.7, *System.NotSupportedException:
5+
Cannot create instance of type ... no Java peer type found* caused certain
6+
apps to crash during launch after incremental deployments.

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -670,8 +670,8 @@ public static string GetClassNameFromInstance (IntPtr jobject)
670670
}
671671
}
672672

673-
[DllImport ("__Internal", CallingConvention = CallingConvention.Cdecl)]
674-
static extern IntPtr monodroid_typemap_managed_to_java (byte[] mvid, int token);
673+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
674+
static extern unsafe IntPtr monodroid_typemap_managed_to_java (Type type, byte* mvid);
675675

676676
internal static void LogTypemapTrace (StackTrace st)
677677
{
@@ -685,19 +685,24 @@ internal static void LogTypemapTrace (StackTrace st)
685685
}
686686
}
687687

688-
internal static string TypemapManagedToJava (Type type)
688+
internal static unsafe string TypemapManagedToJava (Type type)
689689
{
690690
if (mvid_bytes == null)
691691
mvid_bytes = new byte[16];
692692

693-
Span<byte> mvid = new Span<byte>(mvid_bytes);
694-
byte[] mvid_slow = null;
693+
var mvid = new Span<byte>(mvid_bytes);
694+
byte[] mvid_data = null;
695695
if (!type.Module.ModuleVersionId.TryWriteBytes (mvid)) {
696696
monodroid_log (LogLevel.Warn, LogCategories.Default, $"Failed to obtain module MVID using the fast method, falling back to the slow one");
697-
mvid_slow = type.Module.ModuleVersionId.ToByteArray ();
697+
mvid_data = type.Module.ModuleVersionId.ToByteArray ();
698+
} else {
699+
mvid_data = mvid_bytes;
698700
}
699701

700-
IntPtr ret = monodroid_typemap_managed_to_java (mvid_slow == null ? mvid_bytes : mvid_slow, type.MetadataToken);
702+
IntPtr ret;
703+
fixed (byte* mvidptr = mvid_data) {
704+
ret = monodroid_typemap_managed_to_java (type, mvidptr);
705+
}
701706

702707
if (ret == IntPtr.Zero) {
703708
if (LogTypemapMissStackTrace) {

src/Mono.Android/Test/Java.Interop/JnienvTest.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,19 +383,16 @@ public void JavaToManagedTypeMapping ()
383383
Assert.AreEqual (null, m);
384384
}
385385

386-
[DllImport ("__Internal", CallingConvention = CallingConvention.Cdecl)]
387-
static extern IntPtr monodroid_typemap_managed_to_java (byte[] mvid, int token);
388-
389386
[Test]
390387
public void ManagedToJavaTypeMapping ()
391388
{
392389
Type type = typeof(Activity);
393-
var m = monodroid_typemap_managed_to_java (type.Module.ModuleVersionId.ToByteArray (), type.MetadataToken);
394-
Assert.AreNotEqual (IntPtr.Zero, m, "`Activity` subclasses Java.Lang.Object, it should be in the typemap!");
390+
string m = JNIEnv.TypemapManagedToJava (type);
391+
Assert.AreNotEqual (null, m, "`Activity` subclasses Java.Lang.Object, it should be in the typemap!");
395392

396393
type = typeof (JnienvTest);
397-
m = monodroid_typemap_managed_to_java (type.Module.ModuleVersionId.ToByteArray (), type.MetadataToken);
398-
Assert.AreEqual (IntPtr.Zero, m, "`JnienvTest` does *not* subclass Java.Lang.Object, it should *not* be in the typemap!");
394+
m = JNIEnv.TypemapManagedToJava (type);
395+
Assert.AreEqual (null, m, "`JnienvTest` does *not* subclass Java.Lang.Object, it should *not* be in the typemap!");
399396
}
400397

401398
[Test]

src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ void SaveResource (string resource, string filename, string destDir, Func<string
402402
void WriteTypeMappings (List<TypeDefinition> types)
403403
{
404404
var tmg = new TypeMapGenerator ((string message) => Log.LogDebugMessage (message), SupportedAbis);
405-
if (!tmg.Generate (SkipJniAddNativeMethodRegistrationAttributeScan, types, TypemapOutputDirectory, GenerateNativeAssembly, out ApplicationConfigTaskState appConfState))
405+
if (!tmg.Generate (Debug, SkipJniAddNativeMethodRegistrationAttributeScan, types, TypemapOutputDirectory, GenerateNativeAssembly, out ApplicationConfigTaskState appConfState))
406406
throw new XamarinAndroidException (4308, Properties.Resources.XA4308);
407407
GeneratedBinaryTypeMaps = tmg.GeneratedBinaryTypeMaps.ToArray ();
408408
BuildEngine4.RegisterTaskObject (ApplicationConfigTaskState.RegisterTaskObjectKey, appConfState, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false);

src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public class PrepareAbiItems : AndroidTask
2424
[Required]
2525
public bool TypeMapMode { get; set; }
2626

27+
[Required]
28+
public bool Debug { get; set; }
29+
30+
[Required]
31+
public bool InstantRunEnabled { get; set; }
32+
2733
[Output]
2834
public ITaskItem[] AssemblySources { get; set; }
2935

@@ -53,9 +59,11 @@ public override bool RunTask ()
5359
if (!TypeMapMode)
5460
continue;
5561

56-
item = new TaskItem (Path.Combine (NativeSourcesDir, $"{baseName}.{abi}-managed.inc"));
57-
item.SetMetadata ("abi", abi);
58-
includes.Add (item);
62+
if (!InstantRunEnabled && !Debug) {
63+
item = new TaskItem (Path.Combine (NativeSourcesDir, $"{baseName}.{abi}-managed.inc"));
64+
item.SetMetadata ("abi", abi);
65+
includes.Add (item);
66+
}
5967
}
6068

6169
if (haveArmV7SharedSource) {

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,6 @@ public void GenerateJavaStubsAndAssembly ([Values (true, false)] bool isRelease)
832832
readonly string [] ExpectedAssemblyFiles = new [] {
833833
Path.Combine ("android", "environment.armeabi-v7a.o"),
834834
Path.Combine ("android", "environment.armeabi-v7a.s"),
835-
Path.Combine ("android", "typemaps.armeabi-v7a-managed.inc"),
836835
Path.Combine ("android", "typemaps.armeabi-v7a-shared.inc"),
837836
Path.Combine ("android", "typemaps.armeabi-v7a.o"),
838837
Path.Combine ("android", "typemaps.armeabi-v7a.s"),

src/Xamarin.Android.Build.Tasks/Utilities/ARMNativeAssemblerTargetProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class ARMNativeAssemblerTargetProvider : NativeAssemblerTargetProvider
1414
public override string AbiName => Is64Bit ? ARMV8a : ARMV7a;
1515
public override uint MapModulesAlignBits => Is64Bit ? 3u : 2u;
1616
public override uint MapJavaAlignBits { get; } = 2;
17+
public override uint DebugTypeMapAlignBits => Is64Bit ? 3u : 2u;
1718

1819
public ARMNativeAssemblerTargetProvider (bool is64Bit)
1920
{

src/Xamarin.Android.Build.Tasks/Utilities/NativeAssemblerTargetProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ abstract class NativeAssemblerTargetProvider
1111
public abstract string AbiName { get; }
1212
public abstract uint MapModulesAlignBits { get; }
1313
public abstract uint MapJavaAlignBits { get; }
14+
public abstract uint DebugTypeMapAlignBits { get; }
1415

1516
public virtual string MapType <T> ()
1617
{

src/Xamarin.Android.Build.Tasks/Utilities/NativeTypeMappingData.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ namespace Xamarin.Android.Tasks
66
{
77
class NativeTypeMappingData
88
{
9-
public TypeMapGenerator.ModuleData[] Modules { get; }
9+
public TypeMapGenerator.ModuleReleaseData[] Modules { get; }
1010
public IDictionary<string, string> AssemblyNames { get; }
1111
public string[] JavaTypeNames { get; }
12-
public TypeMapGenerator.TypeMapEntry[] JavaTypes { get; }
12+
public TypeMapGenerator.TypeMapReleaseEntry[] JavaTypes { get; }
1313

1414
public uint MapModuleCount { get; }
1515
public uint JavaTypeCount { get; }
1616
public uint JavaNameWidth { get; }
1717

18-
public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleData[] modules, int javaNameWidth)
18+
public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleReleaseData[] modules, int javaNameWidth)
1919
{
2020
Modules = modules ?? throw new ArgumentNullException (nameof (modules));
2121

@@ -24,19 +24,19 @@ public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleData
2424

2525
AssemblyNames = new Dictionary<string, string> (StringComparer.Ordinal);
2626

27-
var tempJavaTypes = new Dictionary<string, TypeMapGenerator.TypeMapEntry> (StringComparer.Ordinal);
27+
var tempJavaTypes = new Dictionary<string, TypeMapGenerator.TypeMapReleaseEntry> (StringComparer.Ordinal);
2828
int managedStringCounter = 0;
2929
var moduleComparer = new TypeMapGenerator.ModuleUUIDArrayComparer ();
3030

31-
foreach (TypeMapGenerator.ModuleData data in modules) {
31+
foreach (TypeMapGenerator.ModuleReleaseData data in modules) {
3232
data.AssemblyNameLabel = $"map_aname.{managedStringCounter++}";
3333
AssemblyNames.Add (data.AssemblyNameLabel, data.AssemblyName);
3434

3535
int moduleIndex = Array.BinarySearch (modules, data, moduleComparer);
3636
if (moduleIndex < 0)
3737
throw new InvalidOperationException ($"Unable to map module with MVID {data.Mvid} to array index");
3838

39-
foreach (TypeMapGenerator.TypeMapEntry entry in data.Types) {
39+
foreach (TypeMapGenerator.TypeMapReleaseEntry entry in data.Types) {
4040
entry.ModuleIndex = moduleIndex;
4141
if (tempJavaTypes.ContainsKey (entry.JavaName))
4242
continue;
@@ -47,7 +47,7 @@ public NativeTypeMappingData (Action<string> logger, TypeMapGenerator.ModuleData
4747
var javaNames = tempJavaTypes.Keys.ToArray ();
4848
Array.Sort (javaNames, StringComparer.Ordinal);
4949

50-
var javaTypes = new TypeMapGenerator.TypeMapEntry[javaNames.Length];
50+
var javaTypes = new TypeMapGenerator.TypeMapReleaseEntry[javaNames.Length];
5151
for (int i = 0; i < javaNames.Length; i++) {
5252
javaTypes[i] = tempJavaTypes[javaNames[i]];
5353
}

0 commit comments

Comments
 (0)