Repository navigation
Support Type & member remapping #867
Description
Scenarios
There are two scenarios for which a "generalized" type & member remapping solution would be useful:
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
MAMActivityin place ofActivityand methods such asOnMAMCreateinstead ofOnCreate).
"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:
- 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.
- 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:
-
JniPeerMembers.JniPeerTypeusesJniType.GetCachedJniType(): https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniPeerMembers.cs#L80-L86 -
JniType.GetCachedJniType()usesJniEnvironment.Types.FindClass():
JniEnvironmentt.Types.FindClass()can hitJniRuntime.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:
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:
The resulting "new style" binding is:
Note in particular the binding for SomeObject.VoidMethodWithParams():
Activity
jonpryor commentedon Aug 11, 2021
Proposal
The
Java.Interop.JniRuntime.JniTypeManagertype should be the public API. Add the following members:These virtual methods would then be overridden by
AndroidTypeManagerin xamarin-android, which allows us to delegate the different-yet-equally-complicated task of obtaining the required data to make this all work, as part of the xamarin-android build process. There, we could add a new@(AndroidRemapConfig)item group, and have it support the Intune MAMremapping-config.jsoninput format.Desugar
GetFallbackType()is for the Desugar case, and this assumes that there's only one "fallback" type to lookup. (Should it instead be anIEnumerable<JniType>??). An appropriate invocation could be added to e.g.JniPeerMembers.JniStaticMethods.GetMethodInfo()if the initial member lookup fails: https://github.com/xamarin/java.interop/blob/ff2714200107fb616828b9d1013f69605791d2ba/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs#L25-L36InTune
GetReplacementType()andGetReplacementMethodInfo()are for the InTune MAM case.ClassRewritesGetReplacementType()is for when the class hierarchy needs to be changed. For example, given thisremapping-config.jsonentry:and the managed binding (built in xamarin-android):
We would update the
JniPeerMembersconstructor: https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/src/Java.Interop/Java.Interop/JniPeerMembers.cs#L37-L65to include a call to
GetReplacementType():Note: doing this means we lose the original type, e.g.
android/app/Activity, and thus allActivitymethod lookups would be performed againstMAMActivity. Is this correct/desirable?To replace the
onCreate()invocation withonMAMCreate(), we'd updateJniPeerMembers.JniInstanceMethodsto callGetReplacementMethodInfo(): https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs#L84-L95For
onCreate()to be remapped toonMAMCreate(), we'd wantGetReplacementMethodInfo()to return:GlobalMethodCallsGetReplacementMethodInfo()is also for theGlobalMethodCallscase. When"MakeStatic": falseis specified, the method is expected to be a static method, and we change which type it's looked up on. This overlaps with the Desugar case, in that changes toJniPeerMembers.JniStaticMethods.GetMethodInfo()is implied, but it can't be identical (meaning we need to "unify" logic somewhat). Given:then when
JniPeerMembers.JniStaticMethods.GetMethodInfo()callsGetReplacementMethodInfo(), we'd wantGetReplacementMethodInfo()to return:This also needs to handle all method overloads.
When
"MakeStatic": trueis specified, we need to turn an instance method invocation into a static method invocation. This overlaps with theClassRewriteschanges toJniPeerMembers.JniInstanceMethods.GetMethodInfo(), with the addition that we need to changeJniMethodInfo:JniMethodInfo.ParameterCountwill be set toReplacementMethodInfo.TargetJniMethodParameterCount.Given the
remapping-config.jsonentry:then we'd want
GetReplacementMethodInfo()to return:Here's where things get "squinty": the binding for
BackupDataInput.readNextHeader()is:_members.InstanceMethods.InvokeVirtualBooleanMethod()is: https://github.com/xamarin/java.interop/blob/d16b1e567e40762b49ccdd62772f2a083be334f2/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs#L62-L78We'd have to update
InvokeVirtual*Method()so that whenm.IsStaticis true -- which is normally unexpected when we're in invoking an instance method -- then we need to re-package the parameters, using the parameter count inJniMethodInfo.ParameterCount:jonpryor commentedon Aug 11, 2021
…though now that I peruse
remapping-config.jsona bit more, I'm not sure what to make of:{ "Class": { "From": "android.content.pm.PackageManager", "To": "com.microsoft.intune.mam.client.content.pm.MAMPackageManagement" }, "Methods": [ { "MakeStatic": true, "OriginalName": "checkPermission" },What does it mean when
"MakeStatic": trueis specified? How does that work?jonpryor commentedon Aug 12, 2021
In retrospect, I don't want
JniEnvironment.Types.FindClass()to do the type name remapping, as that means there's no way to work around the remapping, should that ever be necessary.Instead, type name remapping should be done within
JniPeerMembers.jonpryor commentedon Aug 20, 2021
@jonpryor asked:
The intention is two things: the instance method becomes a static method invocation on the specified type, and a new parameter is inserted at the start. For example, given a Java method invocation of:
the
MakeStaticlogic would update the Java bytecode to become:Two items of note:
checkPermissiontoCheckPermission.managerchanges from an instance to a parameter.4brunu commentedon Dec 19, 2021
Any news on this? 🙂
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
23 remaining items
[Java.Interop] Type & Member Remapping Support
[Xamarin.Android.Build.Tasks] MAM Member Remapping?
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Xamarin.Android.Build.Tasks] MAM Member Remapping?
[Xamarin.Android.Build.Tasks] MAM Member Remapping?
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support
[Xamarin.Android.Build.Tasks] MAM Member Remapping?
[Xamarin.Android.Build.Tasks] MAM Member Remapping?
[Java.Interop] Type & Member Remapping Support
[Java.Interop] Type & Member Remapping Support (#936)
[Xamarin.Android.Build.Tasks] MAM Member Remapping? (#6591)