Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks, monodroid] Optimize member remapping (#…
Browse files Browse the repository at this point in the history
…7059)

Fixes: #7020

Context: f6f11a5

Commit f6f11a5 introduced type & member remapping.  The problem with
its approach was that it used XML as the representation format, which
needed to be parsed during process startup.  This would contribute to
app startup slowdowns if there were any remappings, but even if there
weren't any remappings, the minimum App size increased by ~490KB, due
to the added dependencies on `System.Private.Xml.dll` & more.

This app size increase is not ideal.

Remove most of the `.apk` size increases by moving the remapping info
into `libxamarin-app.so`.  If no remappings are used, then the size
increase to `libxamarin-app.so` is negligible, and the `.apk` size is
only 61KB larger than pre-f6f11a5a.


~~ API Changes ~~

`Android.Runtime.AndroidTypeManager` gains the following members:

	partial class AndroidTypeManager {
	    struct JniRemappingReplacementMethod {
	        public string target_type, target_name;
	        public bool   is_static;
	    }
	    static extern byte* _monodroid_lookup_replacement_type (
	            string jniSimpleReference
	    );
	    static extern JniRemappingReplacementMethod* _monodroid_lookup_replacement_method_info (
	            string jniSourceType,
	            string jniMethodName,
	            string jniMethodSignature
	    );
	}

`AndroidTypeManager._monodroid_lookup_replacement_type()` replaces
the `JNIEnv.ReplacementTypes` dictionary from f6f11a5.

`AndroidTypeManager._monodroid_lookup_replacement_method_info()`
replaces the `JNIEnv.ReplacementMethods` dictionary from f6f11a5.

Both `_monodroid_lookup_replacement_type()` and
`_monodroid_lookup_replacement_method_info()` are P/Invokes into
`libxamarin-app.so`.


~~ `libxamarin-app.so` Changes ~~

The contents of the `@(_AndroidRemapMembers)` item group are now
stored within `libxamarin-app.so`, with the following structure:

	const uint32_t                          jni_remapping_replacement_method_index_entry_count;
	const JniRemappingIndexTypeEntry        jni_remapping_method_replacement_index[];

	const uint32_t                          jni_remapping_replacement_type_count;
	const JniRemappingTypeReplacementEntry  jni_remapping_type_replacements[];

	struct JniRemappingString {
	    const uint32_t                  length;
	    const char                     *str;
	};
	struct JniRemappingReplacementMethod {
	    const char                     *target_type, *target_name;
	    const bool                      is_static;
	};
	struct JniRemappingIndexMethodEntry {
	    JniRemappingString              name, signature;
	    JniRemappingReplacementMethod   replacement
	};
	struct struct JniRemappingIndexTypeEntry {
	    JniRemappingString              name;
	    uint32_t                        method_count;
	    JniRemappingIndexMethodEntry   *methods;
	};
	struct JniRemappingTypeReplacementEntry {
	    JniRemappingString              name;
	    const char                     *replacement;
	};
	const char *
	_monodroid_lookup_replacement_type (const char *);

	const JniRemappingReplacementMethod*
	_monodroid_lookup_replacement_method_info (const char *jniSourceType, const char *jniMethodName, const char *jniMethodSignature);

Referring to the `<replacements/>` XML from f6f11a5 in
`@(_AndroidRemapMembers)`:

  * `//replace-type/@from` fills `JniRemappingTypeReplacementEntry::name`,
    `//replace-type/@to` fills `JniRemappingTypeReplacementEntry::replacement`,
    and `_monodroid_lookup_replacement_type()` performs a linear
    search over `jni_remapping_type_replacements`.

  * `//replace-method/@source-type` fills `JniRemappingIndexTypeEntry::name`,
    `//replace-method/@source-method-name` fills `JniRemappingIndexMethodEntry::name`,
    `//replace-method/@source-method-signature` fills `JniRemappingIndexMethodEntry::signature`,
    `//replace-method/@target-type` fills `JniRemappingReplacementMethod::target_type`,
    `//replace-method/@target-method-name` fills `JniRemappingReplacementMethod::target_name`,
    `//replace-method/@target-method-signature` and
    `//replace-method/@target-method-parameter-count` are *ignored*,
    `//replace-method/@target-method-instance-to-static` fills `JniRemappingReplacementMethod::is_static`,
    and `_monodroid_lookup_replacement_method_info()` performs a
    search over `jni_remapping_method_replacement_index` looking for
    entries with "matching" type names, method names, and method
    signatures, and once a match is found it returns a pointer to the
    `JniRemappingReplacementMethod` instance.

Co-authored-by: Jonathan Peppers <jonathan.peppers@gmail.com>
  • Loading branch information
grendello and jonathanpeppers authored Jun 7, 2022
1 parent 226d750 commit f99fc81
Show file tree
Hide file tree
Showing 35 changed files with 1,091 additions and 316 deletions.
125 changes: 33 additions & 92 deletions src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value)
}

class AndroidTypeManager : JniRuntime.JniTypeManager {
struct JniRemappingReplacementMethod
{
public string target_type;
public string target_name;
public bool is_static;
};

bool jniAddNativeMethodRegistrationAttributePresent;

public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent)
Expand Down Expand Up @@ -317,123 +324,57 @@ protected override IEnumerable<string> GetSimpleReferences (Type type)
};
}

[DllImport (AndroidRuntime.InternalDllName, CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr _monodroid_lookup_replacement_type (string jniSimpleReference);

protected override string? GetReplacementTypeCore (string jniSimpleReference)
{
if (JNIEnv.ReplacementTypes == null) {
if (!JNIEnv.jniRemappingInUse) {
return null;
}
if (JNIEnv.ReplacementTypes.TryGetValue (jniSimpleReference, out var v)) {
return v;

IntPtr ret = _monodroid_lookup_replacement_type (jniSimpleReference);
if (ret == IntPtr.Zero) {
return null;
}
return null;

return Marshal.PtrToStringAnsi (ret);
}

[DllImport (AndroidRuntime.InternalDllName, CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr _monodroid_lookup_replacement_method_info (string jniSourceType, string jniMethodName, string jniMethodSignature);

protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature)
{
if (JNIEnv.ReplacementMethods == null) {
if (!JNIEnv.jniRemappingInUse) {
return null;
}
#if !STRUCTURED
if (!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, jniMethodSignature), out var r) &&
!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, GetMethodSignatureWithoutReturnType ()), out r) &&
!JNIEnv.ReplacementMethods.TryGetValue (CreateReplacementMethodsKey (jniSourceType, jniMethodName, null), out r)) {
return null;
}
ReadOnlySpan<char> replacementInfo = r;

var targetType = GetNextString (ref replacementInfo);
var targetName = GetNextString (ref replacementInfo);
var targetSig = GetNextString (ref replacementInfo);
var paramCountStr = GetNextString (ref replacementInfo);
var isStaticStr = GetNextString (ref replacementInfo);

int? paramCount = null;
if (!paramCountStr.IsEmpty) {
if (!int.TryParse (paramCountStr, 0, System.Globalization.CultureInfo.InvariantCulture, out var count)) {
return null;
}
paramCount = count;
IntPtr retInfo = _monodroid_lookup_replacement_method_info (jniSourceType, jniMethodName, jniMethodSignature);
if (retInfo == IntPtr.Zero) {
return null;
}

bool isStatic = false;
if (isStaticStr.Equals ("true", StringComparison.Ordinal)) {
isStatic = true;
}
var method = new JniRemappingReplacementMethod ();
method = Marshal.PtrToStructure<JniRemappingReplacementMethod>(retInfo);

if (targetSig.IsEmpty && isStatic) {
paramCount = paramCount ?? JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature);
paramCount++;
int? paramCount = null;
if (method.is_static) {
paramCount = JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature) + 1;
jniMethodSignature = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length);
}

return new JniRuntime.ReplacementMethodInfo {
SourceJniType = jniSourceType,
SourceJniMethodName = jniMethodName,
SourceJniMethodSignature = jniMethodSignature,
TargetJniType = targetType.IsEmpty ? jniSourceType : new string (targetType),
TargetJniMethodName = targetName.IsEmpty ? jniMethodName : new string (targetName),
TargetJniMethodSignature = targetSig.IsEmpty ? jniMethodSignature : new string (targetSig),
TargetJniType = method.target_type,
TargetJniMethodName = method.target_name,
TargetJniMethodSignature = jniMethodSignature,
TargetJniMethodParameterCount = paramCount,
TargetJniMethodInstanceToStatic = isStatic,
TargetJniMethodInstanceToStatic = method.is_static,
};
#else
if (!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, jniMethodSignature), out var r) &&
!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, GetMethodSignatureWithoutReturnType ()), out r) &&
!JNIEnv.ReplacementMethods.TryGetValue ((jniSourceType, jniMethodName, null), out r)) {
return null;
}
var targetSig = r.TargetSignature;
var paramCount = r.ParamCount;
if (targetSig == null && r.TurnStatic) {
targetSig = $"(L{jniSourceType};" + jniMethodSignature.Substring ("(".Length);
paramCount = paramCount ?? JniMemberSignature.GetParameterCountFromMethodSignature (jniMethodSignature);
paramCount++;
}
return new JniRuntime.ReplacementMethodInfo {
SourceJniType = jniSourceType,
SourceJniMethodName = jniMethodName,
SourceJniMethodSignature = jniMethodSignature,
TargetJniType = r.TargetType ?? jniSourceType,
TargetJniMethodName = r.TargetName ?? jniMethodName,
TargetJniMethodSignature = targetSig ?? jniMethodSignature,
TargetJniMethodParameterCount = paramCount,
TargetJniMethodInstanceToStatic = r.TurnStatic,
};
#endif // !STRUCTURED

string GetMethodSignatureWithoutReturnType ()
{
int i = jniMethodSignature.IndexOf (')');
return jniMethodSignature.Substring (0, i+1);
}

string GetValue (string? value)
{
return value == null ? "null" : $"\"{value}\"";
}

ReadOnlySpan<char> GetNextString (ref ReadOnlySpan<char> info)
{
int index = info.IndexOf ('\t');
var r = info;
if (index >= 0) {
r = info.Slice (0, index);
info = info.Slice (index+1);
return r;
}
info = default;
return r;
}
}

static string CreateReplacementMethodsKey (string? sourceType, string? methodName, string? methodSignature) =>
new StringBuilder ()
.Append (sourceType)
.Append ('\t')
.Append (methodName)
.Append ('\t')
.Append (methodSignature)
.ToString ();
#endif // NET

delegate Delegate GetCallbackHandler ();
Expand Down
22 changes: 3 additions & 19 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@
using Java.Interop.Tools.TypeNameMappings;
using System.Diagnostics.CodeAnalysis;

#if NET
using ReplacementTypesDict = System.Collections.Generic.Dictionary<string, string>;
using ReplacementMethodsDict = System.Collections.Generic.Dictionary<string, string>;
#endif // NET

namespace Android.Runtime {
#pragma warning disable 0649
struct JnienvInitializeArgs {
Expand All @@ -40,8 +35,7 @@ struct JnienvInitializeArgs {
public int packageNamingPolicy;
public byte ioExceptionType;
public int jniAddNativeMethodRegistrationAttributePresent;
public IntPtr mappingXml;
public int mappingXmlLen;
public bool jniRemappingInUse;
}
#pragma warning restore 0649

Expand All @@ -55,6 +49,7 @@ public static partial class JNIEnv {
static int androidSdkVersion;

static bool AllocObjectSupported;
internal static bool jniRemappingInUse;

static IntPtr grefIGCUserPeer_class;

Expand All @@ -68,11 +63,6 @@ public static partial class JNIEnv {
static AndroidRuntime? androidRuntime;
static BoundExceptionType BoundExceptionType;

#if NET
internal static ReplacementTypesDict? ReplacementTypes;
internal static ReplacementMethodsDict? ReplacementMethods;
#endif // NET

[ThreadStatic]
static byte[]? mvid_bytes;

Expand Down Expand Up @@ -167,6 +157,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)

gref_gc_threshold = args->grefGcThreshold;

jniRemappingInUse = args->jniRemappingInUse;
java_vm = args->javaVm;

version = args->version;
Expand All @@ -178,13 +169,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
gref_class = args->grefClass;
mid_Class_forName = new JniMethodInfo (args->Class_forName, isStatic: true);

#if NET
if (args->mappingXml != IntPtr.Zero) {
var xml = Encoding.UTF8.GetString ((byte*) args->mappingXml, args->mappingXmlLen);
(ReplacementTypes, ReplacementMethods) = MamXmlParser.ParseStrings (xml);
}
#endif // NET

if (args->localRefsAreIndirect == 1)
IdentityHash = v => _monodroid_get_identity_hash_code (Handle, v);
else
Expand Down
1 change: 0 additions & 1 deletion src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@
<Compile Include="Android.Runtime\JObjectRefType.cs" />
<Compile Include="Android.Runtime\JValue.cs" />
<Compile Include="Android.Runtime\Logger.cs" />
<Compile Include="Android.Runtime\MamXmlParser.cs" />
<Compile Include="Android.Runtime\NamespaceMappingAttribute.cs" />
<Compile Include="Android.Runtime\OutputStreamAdapter.cs" />
<Compile Include="Android.Runtime\OutputStreamInvoker.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Xml;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Android.Build.Tasks;

namespace Xamarin.Android.Tasks
{
public class GenerateJniRemappingNativeCode : AndroidTask
{
internal const string JniRemappingNativeCodeInfoKey = ".:!JniRemappingNativeCodeInfo!:.";

internal sealed class JniRemappingNativeCodeInfo
{
public int ReplacementTypeCount { get; }
public int ReplacementMethodIndexEntryCount { get; }

public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMethodIndexEntryCount)
{
ReplacementTypeCount = replacementTypeCount;
ReplacementMethodIndexEntryCount = replacementMethodIndexEntryCount;
}
}

public override string TaskPrefix => "GJRNC";

public ITaskItem RemappingXmlFilePath { get; set; }

[Required]
public string OutputDirectory { get; set; }

[Required]
public string [] SupportedAbis { get; set; }

public bool GenerateEmptyCode { get; set; }

public override bool RunTask ()
{
if (!GenerateEmptyCode) {
if (RemappingXmlFilePath == null) {
throw new InvalidOperationException ("RemappingXmlFilePath parameter is required");
}

Generate ();
} else {
GenerateEmpty ();
}

return !Log.HasLoggedErrors;
}

void GenerateEmpty ()
{
Generate (new JniRemappingAssemblyGenerator (), typeReplacementsCount: 0);
}

void Generate ()
{
var typeReplacements = new List<JniRemappingTypeReplacement> ();
var methodReplacements = new List<JniRemappingMethodReplacement> ();

var readerSettings = new XmlReaderSettings {
XmlResolver = null,
};

using (var reader = XmlReader.Create (File.OpenRead (RemappingXmlFilePath.ItemSpec), readerSettings)) {
if (reader.MoveToContent () != XmlNodeType.Element || reader.LocalName != "replacements") {
Log.LogError ($"Input file `{RemappingXmlFilePath.ItemSpec}` does not start with `<replacements/>`");
} else {
ReadXml (reader, typeReplacements, methodReplacements);
}
}

Generate (new JniRemappingAssemblyGenerator (typeReplacements, methodReplacements), typeReplacements.Count);
}

void Generate (JniRemappingAssemblyGenerator jniRemappingGenerator, int typeReplacementsCount)
{
jniRemappingGenerator.Init ();

foreach (string abi in SupportedAbis) {
string baseAsmFilePath = Path.Combine (OutputDirectory, $"jni_remap.{abi.ToLowerInvariant ()}");
string llFilePath = $"{baseAsmFilePath}.ll";

using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
jniRemappingGenerator.Write (GeneratePackageManagerJava.GetAndroidTargetArchForAbi (abi), sw, llFilePath);
sw.Flush ();
Files.CopyIfStreamChanged (sw.BaseStream, llFilePath);
}
}

BuildEngine4.RegisterTaskObjectAssemblyLocal (
JniRemappingNativeCodeInfoKey,
new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingGenerator.ReplacementMethodIndexEntryCount),
RegisteredTaskObjectLifetime.Build
);
}

void ReadXml (XmlReader reader, List<JniRemappingTypeReplacement> typeReplacements, List<JniRemappingMethodReplacement> methodReplacements)
{
bool haveAllAttributes;

while (reader.Read ()) {
if (reader.NodeType != XmlNodeType.Element) {
continue;
}

haveAllAttributes = true;
if (String.Compare ("replace-type", reader.LocalName, StringComparison.Ordinal) == 0) {
haveAllAttributes &= GetRequiredAttribute ("from", out string from);
haveAllAttributes &= GetRequiredAttribute ("to", out string to);
if (!haveAllAttributes) {
continue;
}

typeReplacements.Add (new JniRemappingTypeReplacement (from, to));
} else if (String.Compare ("replace-method", reader.LocalName, StringComparison.Ordinal) == 0) {
haveAllAttributes &= GetRequiredAttribute ("source-type", out string sourceType);
haveAllAttributes &= GetRequiredAttribute ("source-method-name", out string sourceMethodName);
haveAllAttributes &= GetRequiredAttribute ("target-type", out string targetType);
haveAllAttributes &= GetRequiredAttribute ("target-method-name", out string targetMethodName);
haveAllAttributes &= GetRequiredAttribute ("target-method-instance-to-static", out string targetIsStatic);

if (!haveAllAttributes) {
continue;
}

if (!Boolean.TryParse (targetIsStatic, out bool isStatic)) {
Log.LogError ($"Attribute 'target-method-instance-to-static' in element '{reader.LocalName}' value '{targetIsStatic}' cannot be parsed as boolean; {RemappingXmlFilePath.ItemSpec} line {GetCurrentLineNumber ()}");
continue;
}

string sourceMethodSignature = reader.GetAttribute ("source-method-signature");
methodReplacements.Add (
new JniRemappingMethodReplacement (
sourceType, sourceMethodName, sourceMethodSignature,
targetType, targetMethodName, isStatic
)
);
}
}

bool GetRequiredAttribute (string attributeName, out string attributeValue)
{
attributeValue = reader.GetAttribute (attributeName);
if (!String.IsNullOrEmpty (attributeValue)) {
return true;
}

Log.LogError ($"Attribute '{attributeName}' missing from element '{reader.LocalName}'; {RemappingXmlFilePath.ItemSpec} line {GetCurrentLineNumber ()}");
return false;
}

int GetCurrentLineNumber () => ((IXmlLineInfo)reader).LineNumber;
}
}
}
Loading

0 comments on commit f99fc81

Please sign in to comment.