Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Xamarin.Android.Build.Tasks] MAM Member Remapping? (#6591)
Fixes: dotnet/java-interop#867 Context: dotnet/java-interop@1f27ab5 Context: #6142 (comment) Context: #7020 Changes: dotnet/java-interop@843f3c7...1f27ab5 * dotnet/java-interop@1f27ab55: [Java.Interop] Type & Member Remapping Support (#936) * dotnet/java-interop@02aa54e0: [Java.Interop.Tools.JavaCallableWrappers] marshal method decl types (#987) * dotnet/java-interop@e7bacc37: [ci] Update azure-pipelines.yaml to Pin .NET 6.0.202 (#986) * dotnet/java-interop@fb94d598: [Java.Interop.Tools.JavaCallableWrappers] Collect overriden methods (#985) * dotnet/java-interop@3fcce746: [Java.Interop.{Dynamic,Export}] Nullable Reference Type support (#980) ~~ The Scenarios ~~ Our Java binding infrastructure involves looking up types and methods via [JNI][0], and assumes that types and methods won't "move" in an unexpected manner. Methods which move from a subclass to a superclass works transparently. Methods which are moved to an entirely different class causes us problems. Case in point: [desugaring][0], which can *move* Java types to places that our bindings don't expect. For example, [`Arrays.stream(T[])`][1] may be moved into a `DesugarArrays` class, or a default interface method `Example.m()` may be moved into an `Example$-CC` type. Java.Interop has not supported such changes, resulting in throwing a `Java.Lang.NoSuchMethodError` on Android versions where methods are not where we expect. Additionally, the [InTune Mobile Application Management][3] team needs a expanded type and member lookup mechanism in order to simplify how they maintain their product. Currently they make things work by rewriting IL, which can be brittle. ~~ Build actions ~~ To improve support for this, dotnet/java-interop#936 introduces new `virtual` methods into `Java.Interop.JniRuntime.JniTypeManager` which are called as part of type and member lookup, allowing `AndroidTypeManager` to participate in the type and member resolution process. `AndroidTypeManager` in turn needs to know what types and members can be remapped, and what they should be remapped to. Some of these can be algorithmic, such as pre-pending `Desugar` or appending `$-CC` for the Desugar case. The InTune use case involves a table, contained within the [Microsoft.Intune.MAM.Remapper.Tasks NuGet package][4]. Update `src/Xamarin.Android.Build.Tasks` to add a new `@(_AndroidRemapMembers)` Build action. This build action is not externally supported; it's to help test the feature. Files with this build action are XML files which control type and member remapping: <replacements> <replace-type from="android/app/Activity" to="com/microsoft/intune/mam/client/app/MAMActivity" /> <replace-method source-type="com/microsoft/intune/mam/client/app/MAMActivity" source-method-name="onCreate" source-method-signature="(Landroid/os/Bundle;)V" target-type="com/microsoft/intune/mam/client/app/MAMActivity" target-method-name="onMAMCreate" target-method-instance-to-static="false" /> </replacements> `//replacements/replace-method` is structured with each attribute corresponding to a member on the `JniRuntime.ReplacementMethodInfo` structure, in dotnet/java-interop@1f27ab55. * `//replace-method/@source-type` is `JniRuntime.ReplacementMethodInfo.SourceJniType` * `//replace-method/@source-method-name` is `JniRuntime.ReplacementMethodInfo.SourceJniMethodName` * `//replace-method/@source-method-signature` is `JniRuntime.ReplacementMethodInfo.SourceJniMethodSignature` * `//replace-method/@target-type` is `JniRuntime.ReplacementMethodInfo.TargetJniType` * `//replace-method/@target-method-name` is `JniRuntime.ReplacementMethodInfo.TargetJniMethodName` * `//replace-method/@target-method-signature` is `JniRuntime.ReplacementMethodInfo.TargetJniMethodSignature` This attribute is optional. * `//replace-method/@target-method-parameter-count` is `JniRuntime.ReplacementMethodInfo.TargetJniMethodParameterCount`. This attribute is optional. * `//replace-method/@target-method-instance-to-static` is `JniRuntime.ReplacementMethodInfo.TargetJniMethodIsStatic` `@source-type`, `@source-method-name`, and `@source-method-signature` combined serve as a "key" for looking up the associated `@target-*` information. Update `src/Xamarin.Android.Build.Tasks` to add a new `@(_AndroidMamMappingFile)` Build action. This build action is not externally supported; it's to help test the feature. Files with this build action are expected to be JSON documents which follow the current conventions of `remapping-config.json`, within the `Microsoft.Intune.MAM.Remapper.Tasks` NuGet package. This build action is not externally supported; this is currently for testing purposes. `@(_AndroidMamMappingFile)` files are processed at build time into `@(_AndroidRemapMembers)` XML files. During App builds, all `@(_AndroidRemapMembers)` files are merged into an `@(AndrodAsset)` named `xa-internal/xa-mam-mapping.xml`. This asset is opened and provided to `JNIEnv.Initialize()` as part of native app startup. ~~ Putting it all together ~~ This will only work on .NET 7+. App project has a `@(_AndroidRemapMembers)` file. This item is processed during App build, stored into the `.apk`, and read during app startup on an Android device. Given a Java binding such as: public partial class Activity { static readonly JniPeerMembers _members = new XAPeerMembers ("android/app/Activity", typeof (Activity)); } when the `JniPeerMembers` constructor runs, it will call `JniEnvironment.Runtime.TypeManager.GetReplacementType("android/app/Activity")`. If `@(_AndroidRemapMembers)` is based on the InTune `remapping-config.json` file, then `android/app/Activity` is mapped to `com/microsoft/intune/mam/client/app/MAMActivity`, and `JNIEnv::FindClass()` will be told to lookup `MAMActivity`, *not* `Activity`. *If `MAMActivity` can't be found*, e.g. you're testing this all out, the app will ~immediately crash, as `MAMActivity` doesn't exist. 😅 If `MAMActivity` can be found, eventually `Activity.OnCreate()` will need to be invoked: partial class Activity { protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) { const string __id = "onCreate.(Landroid/os/Bundle;)V"; try { JniArgumentValue* __args = stackalloc JniArgumentValue [1]; __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); } finally { global::System.GC.KeepAlive (savedInstanceState); } } } `_members.InstanceMethods.InvokeVirtualVoidMethod()` will internally make a call similar to: var r = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo ( "com/microsoft/intune/mam/client/app/MAMActivity", "onCreate", "(Landroid/os/Bundle;)V" ); The data returned will be equivalent to: var r = new JniRuntime.ReplacementMethodInfo { SourceJniType = "com/microsoft/intune/mam/client/app/MAMActivity", // from input parameter SourceJniMethodName = "onCreate", // from input parameter SourceJniMethodSignature = "(Landroid/os/Bundle;)V", // from input parameter TargetJniType = "com/microsoft/intune/mam/client/app/MAMActivity", // from //replace-method/@target-type TargetJniMethodName = "onMAMCreate", // from //replace-method/@target-method-name TargetJniMethodSignature = "(Landroid/os/Bundle;)V", // from input parameter, as signature didn't change TargetJniMethodParameterCount = 1, // computed based on signature TargetJniMethodIsStatic = false, // from //replace-method/@target-method-instance-to-static } This will allow `_members.InstanceMethods.InvokeVirtualVoidMethod()` to instead resolve and invoke `MAMActivity.onMAMCreate()`. ~~ Tools ~~ `tools/remap-mam-json-to-xml` is added, and will process the InTune JSON file into `@(_AndroidRemapMembers)` XML: $ dotnet run --project tools/remap-mam-json-to-xml -- \ $HOME/.nuget/packages/microsoft.intune.mam.remapper.tasks/0.1.4635.1/content/MonoAndroid10/remapping-config.json <replacements>… ~~ Unit Tests ~~ `@(_AndroidRemapMembers)` usage is required by `Mono.Android.NET-Tests.apk`, as `Java.Interop-Tests.dll` exercises the type and member remapping logic. ~~ Unrelated `gref+` logging fixes ~~ When `debug.mono.log` = `gref+`, the app could crash: signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x79c045dead This was likely because a constant string was provided to `OSBridge::_write_stack_trace()`, which tried to write into the constant string, promptly blowing things up. Workaround: don't use `gref+` logging when a GC occurs? (Fortunately, `gref+` logging isn't the default.) Better workaround: Don't Do That™. Don't write to const strings. ~~ About `@(_AndroidRemapMembers)` Semantics… ~~ 1. Changing the Java hierarchy "requires" changing the managed hierarchy to mirror it. If we rename `Activity` to `RemapActivity` but *don't* change `MainActivity` to inherit the (bound!) `Example.RemapActivity`, the app *crashes*: JNI DETECTED ERROR IN APPLICATION: can't call void example.RemapActivity.onMyCreate(android.os.Bundle) on instance of example.MainActivity This can be "fixed" *without* changing the base class of `MainActivity` by instead changing the base class of the Java Callable Wrapper for `MainActivity` to `example.RemapActivity`. This can be done manually (just edit the files in `obj/…`!), but isn't really supported in "normal" xamarin-android usage (the next Clean will wipe your changes). Presumably InTune would make this Just Work by e.g. patching the `MainActivity.class` file. 2. `/replacements/replace-type` interacts with `/replacements/replace-method`: at runtime, `//replace-type@from` *no longer exists*, meaning you ***cannot*** use that name in `//replace-method/@source-type` either! If `Activity` is remapped to `RemapActivity`, then *there is no* `Activity.onCreate()` method to similarly remap. Instead, you need to specify `RemapActivity.onCreate()`. This warps the brain a bit. This: <replace-method source-type="example/RemapActivity" source-method-name="onCreate" target-type="example/RemapActivity" target-method-name="onMyCreate" target-method-instance-to-static="false" /> not this: <replace-method source-type="android/app/Activity" source-method-name="onCreate" target-type="example/RemapActivity" target-method-name="onMyCreate" target-method-instance-to-static="false" /> 3. Don't intermix type renames with `/replace-method/@target-method-instance-to-static='true']`. It *can* be done, but also warps the brain. The deal with `@target-method-instance-to-static` is that it it changes the target method signature -- unless explicitly provided in `/replace-method/@target-method-signature` -- so that the "source declaring type" is a prefix. Thus given <replace-method source-type="android/view/View" source-method-name="setOnClickListener" target-type="example/ViewHelper" target-method-name="mySetOnClickListener" target-method-instance-to-static="true" /> we'll look for `ViewHelper.mySetOnClickListener(View, View.OnClickListener)`. If we renamed `View` to `MyView`, we would instead look for `ViewHelper.mySetOnClickListener(MyView, View.OnClickListener)` (note changed parameter type). This almost certainly *won't* work. ~~ InTune Integration Testing? ~~ For "more complete" InTune integration testing, one will want the path to `remapping-config.json`, without hardcoding things. This can be done with `%(PackageReference.GeneratePathProperty)`=True and using `$(PkgMicrosoft_Intune_MAM_Remapper_Tasks)`: <ItemGroup> <PackageReference Include="Microsoft.Intune.MAM.Remapper.Tasks" Version="0.1.4635.1" IncludeAssets="none" GeneratePathProperty="True" ReferenceOutputAssembly="False" /> </ItemGroup> <Target Name="_AddMamFiles" BeforeTargets="_AddAndroidCustomMetaData"> <ItemGroup> <_AndroidMamMappingFile Include="$(PkgMicrosoft_Intune_MAM_Remapper_Tasks)/content/MonoAndroid10/remapping-config.json" /> </ItemGroup> </Target> This is still fraught with some peril, as it likely also depends on getting the right "inner" build, which may require using the plural `$(TargetFrameworks)` property, not the singular `$(TargetFramework)`. This might still be a useful start. ~~ TODO ~~ Optimize this mess: #7020 [0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html [1]: https://developer.android.com/studio/write/java8-support#library-desugaring [2]: https://developer.android.com/reference/java/util/Arrays#stream(T[]) [3]: https://docs.microsoft.com/en-us/mem/intune/fundamentals/what-is-intune [4]: https://www.nuget.org/packages/Microsoft.Intune.MAM.Remapper.Tasks/
- Loading branch information