Skip to content

Commit e5b1c92

Browse files
jonathanpeppersjonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] <GenerateJavaStubs /> generates a shorter acw-map.txt (#1131)
Fixes: https://bugzilla.xamarin.com/show_bug.cgi?id=61073 Context: dotnet/java-interop@429dc2a Methods such as `JNIEnv.GetJniName(Type)` and `TypeManager.GetJavaToManagedType(string)` are used to map `System.Type` values to JNI type references, and to map from JNI type references to `System.Type`-compatible values. Once upon a time this was done through System.Reflection; see [`JavaNativeTypeManager.ToJniName(Type)`][to-jni] and [`JavaNativeTypeManager.ToCliType(string)`][from-jni]. [to-jni]: https://github.com/xamarin/java.interop/blob/5e77d91085820611ce3eda65537a6e7c19df90ef/src/Java.Interop.Tools.TypeNameMappings/Java.Interop.Tools.TypeNameMappings/JavaNativeTypeManager.cs#L147-L151 [from-jni]: https://github.com/xamarin/java.interop/blob/5e77d91085820611ce3eda65537a6e7c19df90ef/src/Java.Interop.Tools.TypeNameMappings/Java.Interop.Tools.TypeNameMappings/JavaNativeTypeManager.cs#L117-L126 Heavy use of reflection was deemed a terrible mistake, so as a fast path we added support for ["type mapping files"][typemap-format], a pair of files generated at packaging time. The `typemap.jm` file contained mappings from JNI type references to Assembly-Qualified type names, while the `typemap.mj` file contained mappings from Assembly-Qualified type names to JNI type references. (Reflection was preserved as a fallback in case we missed something in the introduction of type mapping files.) [typemap-format]: https://github.com/xamarin/java.interop/blob/5e77d91085820611ce3eda65537a6e7c19df90ef/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/TypeNameMapGenerator.cs#L15-L57 For example, `typemap.jm` would contain: android/app/Activity Android.App.Activity, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065 The type mapping files are found in the intermediate dir after a build in `obj/$(Configuration)/acw-map.txt`, or `$(_AcwMapFile)`. (Use `strings` in order to easily read the contents of a file; the type mapping files are in a baroque binary file format.) Unfortunately, Assembly-Qualified names interact with [`AssemblyVersionAttribute`][ava]. `AssemblyVersionAttribute` is used to specify the assembly version, and the C# compiler allows this attribute to contain *wildcards*: [ava]: https://msdn.microsoft.com/en-us/library/system.reflection.assemblyversionattribute(v=vs.110).aspx [assembly: AssemblyVersion ("1.0.0.*")] If the `AssemblyVersionAttribute` contains a wildcard, then the assembly version will change on *every build*. For example, on the first build, there might be an Assembly-Qualified name of: Foo.Bar, Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken= On the next build, it may change to: Foo.Bar, Example, Version=1.0.0.1, Culture=neutral, PublicKeyToken= The type mapping files use the Assembly-Qualified names, but they are only rebuilt when `<GenerateJavaStubs>` executes, which is not necessarily when the assembly changes. (This is intentional for commercial fast deployment support: if the Java Callable Wrappers or type mapping files changed, then the `.apk` would need to be rebuilt and redeployed, slowing down the deployment+debug cycle.) In short, type mapping files are acting as a cache, and that cache can be invalidated by using the `[AssemblyVersion]` custom attribute. The result is that it's possible to raise a `Java.Lang.ClassNotFoundException` by simply rebuilding and re-running a project: 1. Create a new Xamarin.Android application project. 2. Add an `[AssemblyVersion]` custom attribute which contains a wildcard. 3. Run the application in Debug configuration, with fast deployment enabled (which is the default with the commercial SDK). 4. Touch a `.cs` file in the application project, and re-run the app. The app *should* work. Instead, it may fail: JNIEnv.FindClass(Type) caught unexpected exception: Java.Lang.ClassNotFoundException: md50039d44cbb3b194ba4f4e52eaa252795.BrowserPagerAdapter ---> Java.Lang.ClassNotFoundException: Didn't find class "md50039d44cbb3b194ba4f4e52eaa252795.BrowserPagerAdapter" on path: DexPathList[[zip file "/data/app/com.outcoder.browser-1/base.apk"],nativeLibraryDirectories=[/data/app/com.outcoder.browser-1/lib/arm, /vendor/lib, /system/lib]] --- End of inner exception stack trace --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <657aa8fea4454dc898a9e5f379c58734>:0 at Java.Interop.JniEnvironment+StaticMethods.CallStaticObjectMethod (Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x00069] in <54816278eed9488eb28d3597fecd78f8>:0 at Android.Runtime.JNIEnv.CallStaticObjectMethod (System.IntPtr jclass, System.IntPtr jmethod, Android.Runtime.JValue* parms) [0x0000e] in <28e323a707a2414f8b493f6d4bb27c8d>:0 at Android.Runtime.JNIEnv.CallStaticObjectMethod (System.IntPtr jclass, System.IntPtr jmethod, Android.Runtime.JValue[] parms) [0x00017] in <28e323a707a2414f8b493f6d4bb27c8d>:0 at Android.Runtime.JNIEnv.FindClass (System.String classname) [0x0003d] in <28e323a707a2414f8b493f6d4bb27c8d>:0 at Android.Runtime.JNIEnv.FindClass (System.Type type) [0x00015] in <28e323a707a2414f8b493f6d4bb27c8d>:0 --- End of managed Java.Lang.ClassNotFoundException stack trace --- java.lang.ClassNotFoundException: md50039d44cbb3b194ba4f4e52eaa252795.BrowserPagerAdapter at java.lang.Class.classForName(Native Method) at java.lang.Class.forName(Class.java:309) ... Caused by: java.lang.ClassNotFoundException: Didn't find class "md50039d44cbb3b194ba4f4e52eaa252795.BrowserPagerAdapter" on path: DexPathList[[zip file "/data/app/com.outcoder.browser-1/base.apk"],nativeLibraryDirectories=[/data/app/com.outcoder.browser-1/lib/arm, /vendor/lib, /system/lib]] ... Fix this by no longer using Assembly-Qualified names in the type mapping files. Instead, use a *partially* Assembly-Qualified name, which is just the type name and assembly, no version, culture, or PublicKeyToken information: Foo.Bar, Example Bump to Java.Interop/master/429dc2a, which updates `TypeNameMapGenerator` to generate partial Assembly-Qualified names. Update `JNIEnv.GetJniName(Type)` to use a partial Assembly-Qualified name for the `monodroid_typemap_managed_to_java()` invocation. Update the `<GenerateJavaStubs>` task so that it no longer emits lines containing full Assembly-Qualified names. (It was already emitting partially Assembly-Qualified names.) Fix `JnienvTest` so that the correct format is tested. Add a test case to ensure that using `[assembly:AssemblyVersion]` doesn't result in changes `acw-map.txt`.
1 parent eb175c5 commit e5b1c92

File tree

5 files changed

+39
-7
lines changed

5 files changed

+39
-7
lines changed

external/Java.Interop

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ public static string GetJniName (Type type)
880880
{
881881
if (type == null)
882882
throw new ArgumentNullException ("type");
883-
var java = monodroid_typemap_managed_to_java (type.AssemblyQualifiedName);
883+
var java = monodroid_typemap_managed_to_java (type.FullName + ", " + type.Assembly.GetName ().Name);
884884
return java == IntPtr.Zero
885885
? JavaNativeTypeManager.ToJniName (type)
886886
: Marshal.PtrToStringAnsi (java);

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,13 +388,18 @@ public void JavaToManagedTypeMapping ()
388388
[DllImport ("__Internal")]
389389
static extern IntPtr monodroid_typemap_managed_to_java (string java);
390390

391+
string GetTypeName (Type type)
392+
{
393+
return type.FullName + ", " + type.Assembly.GetName ().Name;
394+
}
395+
391396
[Test]
392397
public void ManagedToJavaTypeMapping ()
393398
{
394-
var m = monodroid_typemap_managed_to_java (typeof (Activity).AssemblyQualifiedName);
395-
Assert.AreNotEqual (IntPtr.Zero, m);
396-
m = monodroid_typemap_managed_to_java (typeof (JnienvTest).AssemblyQualifiedName);
397-
Assert.AreEqual (IntPtr.Zero, m);
399+
var m = monodroid_typemap_managed_to_java (GetTypeName (typeof (Activity)));
400+
Assert.AreNotEqual (IntPtr.Zero, m, "`Activity` subclasses Java.Lang.Object, it should be in the typemap!");
401+
m = monodroid_typemap_managed_to_java (GetTypeName (typeof (JnienvTest)));
402+
Assert.AreEqual (IntPtr.Zero, m, "`JnienvTest` does *not* subclass Java.Lang.Object, it should *not* be in the typemap!");
398403
}
399404

400405
[Test]

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ void Run (DirectoryAssemblyResolver res)
163163
string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.');
164164

165165
acw_map.WriteLine ("{0};{1}", type.GetPartialAssemblyQualifiedName (), javaKey);
166-
acw_map.WriteLine ("{0};{1}", type.GetAssemblyQualifiedName (), javaKey);
167166

168167
TypeDefinition conflict;
169168
if (managed.TryGetValue (managedKey, out conflict)) {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.IO;
44
using System.Linq;
5+
using System.Reflection;
56
using System.Text;
67
using System.Text.RegularExpressions;
78
using System.Threading.Tasks;
@@ -174,6 +175,33 @@ public void BuildApplicationWithLibraryAndClean ([Values (false, true)] bool isR
174175
}
175176
}
176177

178+
[Test]
179+
public void BuildIncrementingAssemblyVersion ()
180+
{
181+
var proj = new XamarinAndroidApplicationProject ();
182+
proj.Sources.Add (new BuildItem ("Compile", "AssemblyInfo.cs") {
183+
TextContent = () => "[assembly: System.Reflection.AssemblyVersion (\"1.0.0.*\")]"
184+
});
185+
186+
using (var b = CreateApkBuilder ("temp/BuildIncrementingAssemblyVersion")) {
187+
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
188+
189+
var acwmapPath = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "acw-map.txt");
190+
var assemblyPath = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, "UnnamedProject.dll");
191+
var firstAssemblyVersion = AssemblyName.GetAssemblyName (assemblyPath).Version;
192+
var expectedAcwMap = File.ReadAllText (acwmapPath);
193+
194+
b.Target = "Rebuild";
195+
b.BuildLogFile = "rebuild.log";
196+
Assert.IsTrue (b.Build (proj), "Rebuild should have succeeded.");
197+
198+
var secondAssemblyVersion = AssemblyName.GetAssemblyName (assemblyPath).Version;
199+
Assert.AreNotEqual (firstAssemblyVersion, secondAssemblyVersion);
200+
var actualAcwMap = File.ReadAllText (acwmapPath);
201+
Assert.AreEqual (expectedAcwMap, actualAcwMap);
202+
}
203+
}
204+
177205
[Test]
178206
public void BuildMkBundleApplicationRelease ()
179207
{

0 commit comments

Comments
 (0)