Bug 2442 - crash on instantiating a class whose inheritance chain differs between target & runtime SDKs
Summary: crash on instantiating a class whose inheritance chain differs between target...
Status: RESOLVED NOT_ON_ROADMAP
Alias: None
Product: Android
Classification: Xamarin
Component: Mono runtime / AOT Compiler ()
Version: 4.0
Hardware: PC Mac OS
: High normal
Target Milestone: ---
Assignee: Jonathan Pryor
URL:
Depends on:
Blocks:
 
Reported: 2011-12-09 15:10 UTC by adam.lickel
Modified: 2011-12-12 15:18 UTC (History)
2 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:


Attachments
Stacktrace & Example C# & Java Projects (26.75 KB, application/zip)
2011-12-09 15:10 UTC, adam.lickel
Details


Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on Developer Community or GitHub with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED NOT_ON_ROADMAP

Description adam.lickel 2011-12-09 15:10:46 UTC
Created attachment 1025 [details]
Stacktrace & Example C# & Java Projects

Instantiating a class whose inheritance chain has changed can result in a crash. 

I know of two instances at least:
LinkMovementMethod, MotionEvent

When built with Honeycomb and Gingerbread (respectively) these classes cannot
be instantiated on a downlevel version of the OS. 
The only difference is that the inheritance chain was modified to add a new
base class.
This is non-obvious and different from the Java behavior. This it is perfectly legal in Java as the class exists in both the build & runtime.

In MonoDroid I see a TargetInvocationException about how that base class does
not exist (see attached).

I also attached source projects for both java & monodroid which should behave
similarly.
Comment 1 Jonathan Pryor 2011-12-12 15:18:29 UTC
The solution here is to set your TargetFrameworkVersion to the lowest API level you want to support (or avoid use of types for which the inheritance chain has changed). If you want to support API level 7 and higher, then your TargetFrameworkVersion should likewise be Android v2.1 (API level 7), as this will reference a Mono.Android.dll which does _not_ have the InputEvent type, and thus won't throw a NoClassDefFoundError at runtime.

Discussion:

Your Java and C# example code aren't the same. Java:

    private static boolean FAILURE_BEFORE_HONEYCOMB = false;
    ...
    MovementMethod method = LinkMovementMethod.getInstance();
    if (FAILURE_BEFORE_HONEYCOMB) {
        method = new BaseMovementMethod();
    }

C#: 

    var method = LinkMovementMethod.Instance;
#if __ANDROID_11__ && FAILURE_BEFORE_HONEYCOMB
    method = new BaseMovementMethod();
#endif

#if is not the same as a runtime check. ;-)

In this case, changing the C# code to use a runtime check (as the Java code does) would still fail at runtime . However, the "vice versa" case _does_ fail if the Java code executes `new BaseMovementMethod()` on a downlevel platform, e.g. on API level 10:

> E/AndroidRuntime(  351): FATAL EXCEPTION: main
> E/AndroidRuntime(  351): java.lang.NoClassDefFoundError: android.text.method.BaseMovementMethod
> E/AndroidRuntime(  351): 	at com.example.MainActivity.onCreate(MainActivity.java:21)
> E/AndroidRuntime(  351): 	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
> E/AndroidRuntime(  351): 	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)
> ...

Which isn't to say that the original Java code is perfect as-is; executing it on my Nexus One elicits the following (non-fatal) output:

> E/dalvikvm(32661): Could not find class 'android.text.method.BaseMovementMethod', referenced from method com.example.MainActivity.onCreate
> W/dalvikvm(32661): VFY: unable to resolve new-instance 3 (Landroid/text/method/BaseMovementMethod;) in Lcom/example/MainActivity;
> D/dalvikvm(32661): VFY: replacing opcode 0x22 at 0x0010
> D/dalvikvm(32661): VFY: dead code 0x0012-0014 in Lcom/example/MainActivity;.onCreate (Landroid/os/Bundle;)V
> I/AndroidProject(32661): Method: android.text.method.LinkMovementMethod@4051a480

In short, your Java code only works because Dalvik is ignoring the invalid BaseMovementMethod type reference in the bytecode. How kind of it. (I do wonder what would happen with the "saner and error checking" API level 14 Dalvik VM, but we'll need to wait until API level 15 is out to test that...)

As for the exception with C#, this is a known issue. I'm not entirely sure how to fix it given our current architecture (thus closing as WONTFIX).

Background:

The problem is as follows: our current binding generator takes an API description based on android.jar, and generates a mess of C# code rather like this:

    // Assume API level 14
    [global::Android.Runtime.Register ("android/view/InputEvent", DoNotGenerateAcw=true)]
    public abstract partial class InputEvent : Java.Lang.Object, Android.OS.IParcelable {
        internal static IntPtr class_ref = JNIEnv.FindClass ("android/view/InputEvent");
        // ...
    }

    [global::Android.Runtime.Register ("android/view/MotionEvent", DoNotGenerateAcw=true)]
    public sealed partial class MotionEvent : Android.Views.InputEvent, Android.OS.IParcelable {
        internal static IntPtr class_ref = JNIEnv.FindClass ("android/view/MotionEvent");
        // ...
    }

For comparison, the API Level 7 Mono.Android.dll would contain only the MotionEvent type, not the InputEvent type, and MotionEvent subclasses Java.Lang.Object.

The `class_ref` values are initialized in the static constructor, so referencing the MotionEvent type will cause the InputEvent static constructor to run, which will attempt to load the InputEvent type, which will die with a java.lang.NoClassDefFoundError on downlevel platforms.

Solution?

Given our current infrastructure, the one "solution" to this that I can think of would be to make the following changes:

1. Change JNIEnv.FindClass() to return `null` if the type isn't found instead of throwing an exception

2. Change the .jar-parser so that it reports method overrides. The AOSP output does _not_ do this, and the initial .jar parser we wrote was aiming for AOSP compatibility (to minimize differences from prior versions, and to reduce the size of Mono.Android.dll -- generating code for _every_ method override would increase the size of the assembly, though I don't know by how much). The current .jar parser may be generating method overrides in some circumstances (e.g. MotionEvent.getDeviceId() is listed, probably because it's made `final`), but I know that for ScrollingMovementMethod and LinkMovementMethod it is not listing any method overrides. For example, see the current AOSP output:

https://raw.github.com/android/platform_frameworks_base/master/api/14.txt

Search for "public class LinkMovementMethod", and you'll notice that the class declaration they provide doesn't list e.g. onTakeFocus(), which was present as a LinkMovementMethod method in API level 1. The same is true for the ScrollingMovementMethod declaration.

(1) means that instead of a "nice" NoClassDefFoundError exception, looking up types and members that don't exist on the device will result in NullPointerExceptions. I'm not entirely sure that this is a Good Thing; it could probably be considered Bad™.

(2) is very likely doable, but the initial versions of our .jar parser were aiming for AOSP-compatibility (generating the same set of members as the AOSP XML contained), so I don't know how easy it will be to ensure that all overrides are actually found. Furthermore, it will very likely increase the size of (the already gigantic) Mono.Android.dll (to register all the "new" method overrides), though (again) I don't know how much this would actually add.

If an override is missed, the generator would find the (nonexistent) base class method, which would attempt to use a null class_ref variable, and things don't work, so this is fairly brittle.

Finally, (2) implicitly requires that Google, when moving a Java method to a base type, _also_ retain the method in the derived class (if only as a method that calls `super.method(args)`. If they don't, then our .jar parser won't find the override, and it won't get overridden anyway.

In short, it looks _really_ brittle. (Though lots of things about this process are brittle, frankly, but ensuring that you set your TargetFrameworkVersion to the minimum API level you wish to support will _not_ result in NoClassDefFoundErrors, and will continue working on new Android versions as well. It's only when you build against a "new" android.jar and run on an "old" device that things are potentially broken; running older code on newer platforms will work.)

The only alternative solution I can think of involves overriding _every_ virtual method on every derived type; this will _really_ bloat the size of Mono.Android.dll.