Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-tools/create-packs/Microsoft.Android.Runtime.proj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ projects that use the Microsoft.Android framework in .NET 6+.
Include="$(_MonoAndroidNETOutputRoot)$(AndroidLatestStableApiLevel)\Microsoft.Android.Runtime.NativeAOT.dll"
Condition=" '$(AndroidRuntime)' == 'NativeAOT' "
/>
<_AndroidRuntimePackAssemblies
Include="$(_MonoAndroidNETOutputRoot)$(AndroidLatestStableApiLevel)\System.IO.Hashing.dll"
Condition=" '$(AndroidRuntime)' == 'NativeAOT' "
/>
<_AndroidRuntimePackAssemblies Include="$(_MonoAndroidNETOutputRoot)$(AndroidLatestStableApiLevel)\Mono.Android.Export.dll" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,8 @@ partial class NativeAotTypeManager : JniRuntime.JniTypeManager {
internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes;

readonly IDictionary<string, Type> TypeMappings = new Dictionary<string, Type> (StringComparer.Ordinal);

public NativeAotTypeManager ()
{
var startTicks = global::System.Environment.TickCount;
InitializeTypeMappings ();
var endTicks = global::System.Environment.TickCount;
AndroidLog.Print (AndroidLogLevel.Info, "NativeAotTypeManager", $"InitializeTypeMappings() took {endTicks - startTicks}ms");
}

void InitializeTypeMappings ()
{
// Should be replaced by src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs
throw new InvalidOperationException ("TypeMappings should be replaced during trimming!");
}

[return: DynamicallyAccessedMembers (Constructors)]
Expand Down Expand Up @@ -139,7 +127,7 @@ public override void RegisterNativeMembers (

protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
if (TypeMappings.TryGetValue (jniSimpleReference, out var target)) {
if (TypeMapping.TryGetType (jniSimpleReference, out var target)) {
yield return target;
}
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) {
Expand All @@ -150,15 +138,7 @@ protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpl
protected override IEnumerable<string> GetSimpleReferences (Type type)
{
return base.GetSimpleReferences (type)
.Concat (CreateSimpleReferencesEnumerator (type));
}

IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
{
foreach (var e in TypeMappings) {
if (e.Value == type)
yield return e.Key;
}
.Concat (TypeMapping.GetJavaClassNames (type));
}

static int CountMethods (ReadOnlySpan<char> methodsSpan)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO.Hashing;
using System.Runtime.InteropServices;
using System.Text;
using Android.Runtime;

namespace Microsoft.Android.Runtime;

internal static class TypeMapping
{
private static Dictionary<Type, string[]> s_javaClassNames = new ();

internal static bool TryGetType (string javaClassName, [NotNullWhen (true)] out Type? type)
{
ulong hash = Hash (javaClassName);

// the hashes array is sorted and all the hashes are unique
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you going to check the uniqueness of the hashcodes at build time to guarantee this?

An alternative way to deal with this would be to allow non-unique hashcodes and check all candidates in the map. If you do that, you can change the hashcodes to 32-bit that makes the map 2x smaller and dealing with hashcodes faster, at the cost of a rare hash collision.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you going to check the uniqueness of the hashcodes at build time to guarantee this?

Yes, the check is here: https://github.com/dotnet/android/pull/9856/files#diff-8e22a99ebc58c9417a48013888fc90d72fa6b638aff5d2d449cbf2c78bd0a8fbR149-R161

If you do that, you can change the hashcodes to 32-bit that makes the map 2x smaller and dealing with hashcodes faster, at the cost of a rare hash collision.

That's definitely worth trying. Assuming even the XxHash32 collisions are very rare, in the typical case there should be just 1 string comparison to verify that the hash belongs to the expected result.

int index = MemoryExtensions.BinarySearch (Hashes, hash);
if (index < 0) {
type = null;
return false;
}

// ensure this is not a hash collision
string resolvedJavaClassName = GetJavaClassNameByIndex (index);
if (!resolvedJavaClassName.Equals (javaClassName, StringComparison.Ordinal)) {
type = null;
return false;
}

type = GetTypeByIndex (index);
return type is not null;
}

internal static IEnumerable<string> GetJavaClassNames (Type type)
{
if (!s_javaClassNames.TryGetValue (type, out var javaClassNames)) {
javaClassNames = GetJavaClassNamesSlow (type).ToArray ();
_ = s_javaClassNames.TryAdd (type, javaClassNames);
}

return javaClassNames;
}

private static IEnumerable<string> GetJavaClassNamesSlow (Type type)
{
for (int i = 0; i < Hashes.Length; i++) {
if (GetTypeByIndex (i) == type) {
var javaClassName = TypeMapping.GetJavaClassNameByIndex (i);
if (javaClassName is not null) {
yield return javaClassName;
}
}
}
}

private static ulong Hash (string javaClassName)
{
ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes(javaClassName.AsSpan ());
ulong hash = XxHash3.HashToUInt64 (bytes);

// The bytes in the hashes array are stored as little endian. If the target platform is big endian,
// we need to reverse the endianness of the hash.
if (!BitConverter.IsLittleEndian) {
hash = BinaryPrimitives.ReverseEndianness (hash);
}

return hash;
}

// Replaced by src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs
private static ReadOnlySpan<ulong> Hashes => throw new NotImplementedException ();
private static Type? GetTypeByIndex (int index) => throw new NotImplementedException ();
private static string? GetJavaClassNameByIndex (int index) => throw new NotImplementedException ();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<ProjectReference Include="..\Mono.Android\Mono.Android.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.IO.Hashing" Version="$(SystemIOHashingPackageVersion)" />
</ItemGroup>

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

<!-- Copy runtime assemblies to bin/$(Configuration)/dotnet/packs folder -->
Expand All @@ -34,6 +38,7 @@
<Target Name="_CopyToPackDirs">
<ItemGroup>
<_RuntimePackFiles Include="$(OutputPath)Microsoft.Android.Runtime.NativeAOT.dll" AndroidRID="%(AndroidAbiAndRuntimeFlavor.AndroidRID)" AndroidRuntime="%(AndroidAbiAndRuntimeFlavor.AndroidRuntime)" />
<_RuntimePackFiles Include="$(OutputPath)System.IO.Hashing.dll" AndroidRID="%(AndroidAbiAndRuntimeFlavor.AndroidRID)" AndroidRuntime="%(AndroidAbiAndRuntimeFlavor.AndroidRuntime)" />
</ItemGroup>
<Message Importance="high" Text="$(TargetPath) %(AndroidAbiAndRuntimeFlavor.AndroidRID)" />
<Copy
Expand Down
Loading
Loading