Skip to content

Support Type & member remapping #867

@jonpryor

Description

@jonpryor

Scenarios

There are two scenarios for which a "generalized" type & member remapping solution would be useful:

  1. Desugaring
  2. Microsoft Intune MAM

Desugaring

See also: dotnet/android#4574 (comment)
See also: dotnet/android#6142 (comment)

The Android Runtime is responsible for running the Dalvik bytecode within e.g. classes.dex. The Android runtime and Dalvik bytecode formats have apparently changed over time in order to support newer Java language features, such as support for static interface methods:

package org.webrtc;

public /* partial */ interface EGLBase {
    public static EGLBase create() { /* … */}
}

Support for static interface methods was added in Java 8 and in Android v8.0 (API-26). If you want to use static interface methods on an Android device running API-25 or earlier, you must desugar the Java bytecode. The result of desugaring the above org.webrtc.EGLBase type is that the static methods are moved to a new type, with a $-CC suffix, "as if" the Java code were instead:

package org.webrtc;

public /* partial */ interface EGLBase {
    // …
}

public final /* partial */ class EGLBase$-CC {
    public static EGLBase create { /* … */ }
}

While this works for pure Java code, this does not work with Xamarin.Android, because our bindings currently expect (require) that the types & members our binding process detects don't "move":

// "Binding" for EGLBase
namespace Org.Webrtc {
    public partial interface IEGLBase {
        private static readonly JniPeerMembers _members = new JniPeerMembers ("org/webrtc/EGLBase", typeof (IEGLBase));
        public static IEGLBase Create()
        {
            const string __id = "create.()Lorg/webrtc/EglBase;"
            var __rm = _members.StaticMethods.InvokeObjectMethod (__id, null);
            return Java.Lang.Object.GetObject<IEGLBase>(__rm.Handle, JniHandleOwnership.TransferLocalRef);
        }
    }
}

The new JniPeerMembers(…) invocation will use JNIEnv::FindClass() to lookup the org/webrtc/EGLBase type, and IEGLBase.Create() will use JNIEnv::GetStaticMethodID() and JNIEnv::CallStaticObjectMethod() to lookup and invoke the EGLBase.create() method.

However, when desugaring is used, there is no EGLBase.create() method. Consequently, in a "desugared" environment, a Java.Lang.NoSuchMethodError will be thrown when IEGLBase.Create() is invoked, as EGLBase.create() doesn't exist; it "should" instead be looking for EGLBase$-CC.create()!

Microsoft Intune MAM

See also: https://www.nuget.org/packages/Microsoft.Intune.MAM.Remapper.Tasks/
See also: https://docs.microsoft.com/en-us/mem/intune/developer/app-sdk-xamarin#remapper

The Microsoft Intune App SDK Xamarin Bindings is in a similar-yet-different position: it involves Java & Dalvik bytecode manipulation for various security purposes, and in order to make that work reasonably from Xamarin.Android, they also rewrite Xamarin.Android IL so that it's consistent with the manipulated Dalvik bytecode.

See readme.md and content/MonoAndroid10/remapping-config.json within the Microsoft.Intune.MAM.Remapper.Tasks NuGet package for some additional details/tidbits such as:

This task operates on assemblies to replace the base type classes which Microsoft Intune requires to implement its SDK (for example, Intune requires using a MAMActivity in place of Activity and methods such as OnMAMCreate instead of OnCreate).

      "Class": {
        "From": "android.app.Activity",
        "To": "com.microsoft.intune.mam.client.app.MAMActivity"
      },
      "Methods": [
        {
          "MakeStatic": false,
          "OriginalName": "onCreate",
          "NewName": "onMAMCreate",
          "OriginalParams": [
            "android.os.Bundle"
          ]
        },

Discussion

Unfortunately use cases (1) and (2) differ in two significant ways:

  1. Desugaring requires a "primary" type and a "fallback" type for member lookup purposes. MAM doesn't require more than one type be involved, once the correct type has been looked up.
  2. Desugaring doesn't change method names or signatures, just the declaring type (as currently known…?). MAM does change method names and signatures.

Regardless, from the binding side, the primary API of interest is Java.Interop.JniPeerMembers: this is the type through which member bindings "happen" (see IEGLBase type, above, for an example, or see unit tests).

The problem with JniPeerMembers is that it isn't modifiable; assuming that IL rewriting isn't employed, there's no way to change the behavior of JniPeerMembers at runtime.

For class lookup, there is an "extensibility point", kinda: JniRuntime.ClassLoader_LoadClass: https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniRuntime.cs#L176 ; see also commit b4d44e4.

The execution path is:

  1. JniPeerMembers.JniPeerType uses JniType.GetCachedJniType(): https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniPeerMembers.cs#L80-L86

  2. JniType.GetCachedJniType() uses JniEnvironment.Types.FindClass():

https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniType.cs#L88-L97

https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniType.cs#L35-L39

  1. JniEnvironmentt.Types.FindClass() can hit JniRuntime.ClassLoader_LoadClass: https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs#L63-L86

However, this isn't a generalized lookup path; it exists because, on (older versions of?) Android, JNIEnv::FindClass() would only reliably lookup types from the main thread. Other threads wouldn't find types in classes.dex, and thus ClassLoader.loadClass() was needed instead.

Additionally, the use of ClassLoader.loadClass() is only done after trying JNIEnv::FindClass(). This isn't a (current) fix. However, it could be the "path" toward an actual fix.

Additionally, the above is only for Managed-to-Java invocation purposes. We also need to consider Java-to-Managed method registration support, as the Intune MAM tooling will e.g. rename the Java Activity.onCreate() method to Activity.onMAMCreate(). However (?), given that our normal Java Callable Wrapper generation output uses a private final method, this might not in fact matter, as we only register the native methods, not the public methods:

https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs#L130-L169

i.e. Intune MAM would (presumably) only update IndirectApplication.onCreate(), but not IndirectApplication.onCreate.n_onCreate(). TODO: verify this.

So long as the native methods aren't changed and parameter types aren't changed, Java-to-Managed method registration shouldn't require any changes.

Binding Background

Given an API description of:

https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/tests/generator-Tests/expected/NormalMethods/NormalMethods.xml#L18-L65

The resulting "new style" binding is:

https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/tests/generator-Tests/expected.ji/NormalMethods/Xamarin.Test.SomeObject.cs

Note in particular the binding for SomeObject.VoidMethodWithParams():

https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/tests/generator-Tests/expected.ji/NormalMethods/Xamarin.Test.SomeObject.cs#L254-L268

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementProposed change to current functionalityjava-interopRuntime bridge between .NET and Java

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions