Skip to content

Commit

Permalink
JNI remapping native code generator
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Peppers <jonathan.peppers@gmail.com>
  • Loading branch information
grendello and jonathanpeppers committed Jun 6, 2022
1 parent e1af958 commit db15d6e
Show file tree
Hide file tree
Showing 33 changed files with 1,037 additions and 229 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 db15d6e

Please sign in to comment.