Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] <GenerateJavaStubs /> generates a short…
Browse files Browse the repository at this point in the history
…er acw-map.txt (dotnet#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`.
  • Loading branch information
jonathanpeppers authored and jonpryor committed Dec 21, 2017
1 parent eb175c5 commit e5b1c92
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 7 deletions.
2 changes: 1 addition & 1 deletion external/Java.Interop
2 changes: 1 addition & 1 deletion src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ public static string GetJniName (Type type)
{
if (type == null)
throw new ArgumentNullException ("type");
var java = monodroid_typemap_managed_to_java (type.AssemblyQualifiedName);
var java = monodroid_typemap_managed_to_java (type.FullName + ", " + type.Assembly.GetName ().Name);
return java == IntPtr.Zero
? JavaNativeTypeManager.ToJniName (type)
: Marshal.PtrToStringAnsi (java);
Expand Down
13 changes: 9 additions & 4 deletions src/Mono.Android/Test/Java.Interop/JnienvTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,18 @@ public void JavaToManagedTypeMapping ()
[DllImport ("__Internal")]
static extern IntPtr monodroid_typemap_managed_to_java (string java);

string GetTypeName (Type type)
{
return type.FullName + ", " + type.Assembly.GetName ().Name;
}

[Test]
public void ManagedToJavaTypeMapping ()
{
var m = monodroid_typemap_managed_to_java (typeof (Activity).AssemblyQualifiedName);
Assert.AreNotEqual (IntPtr.Zero, m);
m = monodroid_typemap_managed_to_java (typeof (JnienvTest).AssemblyQualifiedName);
Assert.AreEqual (IntPtr.Zero, m);
var m = monodroid_typemap_managed_to_java (GetTypeName (typeof (Activity)));
Assert.AreNotEqual (IntPtr.Zero, m, "`Activity` subclasses Java.Lang.Object, it should be in the typemap!");
m = monodroid_typemap_managed_to_java (GetTypeName (typeof (JnienvTest)));
Assert.AreEqual (IntPtr.Zero, m, "`JnienvTest` does *not* subclass Java.Lang.Object, it should *not* be in the typemap!");
}

[Test]
Expand Down
1 change: 0 additions & 1 deletion src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ void Run (DirectoryAssemblyResolver res)
string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.');

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

TypeDefinition conflict;
if (managed.TryGetValue (managedKey, out conflict)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
Expand Down Expand Up @@ -174,6 +175,33 @@ public void BuildApplicationWithLibraryAndClean ([Values (false, true)] bool isR
}
}

[Test]
public void BuildIncrementingAssemblyVersion ()
{
var proj = new XamarinAndroidApplicationProject ();
proj.Sources.Add (new BuildItem ("Compile", "AssemblyInfo.cs") {
TextContent = () => "[assembly: System.Reflection.AssemblyVersion (\"1.0.0.*\")]"
});

using (var b = CreateApkBuilder ("temp/BuildIncrementingAssemblyVersion")) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");

var acwmapPath = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "acw-map.txt");
var assemblyPath = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, "UnnamedProject.dll");
var firstAssemblyVersion = AssemblyName.GetAssemblyName (assemblyPath).Version;
var expectedAcwMap = File.ReadAllText (acwmapPath);

b.Target = "Rebuild";
b.BuildLogFile = "rebuild.log";
Assert.IsTrue (b.Build (proj), "Rebuild should have succeeded.");

var secondAssemblyVersion = AssemblyName.GetAssemblyName (assemblyPath).Version;
Assert.AreNotEqual (firstAssemblyVersion, secondAssemblyVersion);
var actualAcwMap = File.ReadAllText (acwmapPath);
Assert.AreEqual (expectedAcwMap, actualAcwMap);
}
}

[Test]
public void BuildMkBundleApplicationRelease ()
{
Expand Down

0 comments on commit e5b1c92

Please sign in to comment.