Skip to content

Commit 4991cc9

Browse files
authored
[CoreCLR] java to managed typemap (#10075)
Finish implementing java-to-managed typemap lookup for Debug builds (started in #10065). Maps translate Java type name to a pair consisting of the managed type's assembly name and its token ID within that assembly. This is then used by the managed land to find and load the indicated type. Optimize, by using hashes, the managed-to-java typemap lookup for Debug builds, with a fallback to string-based lookups if hash clashes are detected. Since it is hard to produce a test for such clashes, instead add a way to force usage of string-based lookups by checking whether the `CI_TYPEMAP_DEBUG_USE_STRINGS` environment variable is present and not empty in the environment. This is used in one of the tests. Additionally, optimize usage of certain sets of strings in native code, namely type and assembly names. Instead of storing them as "standard" string pointers, they are now contained in "blobs" - arrays of character data, separated with `NUL` characters. This approach gets rid of a potentially large number of native code relocations (each string is a pointer), replacing X pointers with a single pointer + offset when accessing a string. It also has a side effect of (slightly) reducing generated code size.
1 parent e6e83da commit 4991cc9

File tree

16 files changed

+573
-143
lines changed

16 files changed

+573
-143
lines changed

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using NUnit.Framework;
22
using NUnit.Framework.Interfaces;
33
using System;
4+
using System.Collections.Generic;
45
using System.Globalization;
56
using System.Linq;
67
using System.Diagnostics;
@@ -207,10 +208,10 @@ protected static string AdbStartActivity (string activity)
207208
return RunAdbCommand ($"shell am start -S -n \"{activity}\"");
208209
}
209210

210-
protected static void RunProjectAndAssert (XamarinAndroidApplicationProject proj, ProjectBuilder builder, string logName = "run.log", bool doNotCleanupOnUpdate = false, string [] parameters = null)
211+
protected static void RunProjectAndAssert (XamarinAndroidApplicationProject proj, ProjectBuilder builder, string logName = "run.log", Dictionary<string, string> environmentVariables = null, bool doNotCleanupOnUpdate = false, string [] parameters = null)
211212
{
212213
builder.BuildLogFile = logName;
213-
Assert.True (builder.RunTarget (proj, "Run", doNotCleanupOnUpdate: doNotCleanupOnUpdate, parameters: parameters), "Project should have run.");
214+
Assert.True (builder.RunTarget (proj, "Run", doNotCleanupOnUpdate: doNotCleanupOnUpdate, parameters: parameters, environmentVariables: environmentVariables), "Project should have run.");
214215
}
215216

216217
protected static void StartActivityAndAssert (XamarinAndroidApplicationProject proj)

src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ bool IsValueAssignableFrom (Type valueType, LlvmIrVariable variable)
374374

375375
ulong GetAggregateValueElementCount (GeneratorWriteContext context, Type type, object? value, LlvmIrGlobalVariable? globalVariable = null)
376376
{
377+
if (type == typeof(LlvmIrStringBlob)) {
378+
return 1; // String blobs are a collection of bytes
379+
}
380+
377381
if (!type.IsArray ()) {
378382
throw new InvalidOperationException ($"Internal error: unknown type {type} when trying to determine aggregate type element count");
379383
}
@@ -479,6 +483,11 @@ void WriteType (GeneratorWriteContext context, Type type, object? value, out Llv
479483
return;
480484
}
481485

486+
if (type == typeof(LlvmIrStringBlob)) {
487+
WriteStringBlobType (context, (LlvmIrStringBlob?)value, out typeInfo);
488+
return;
489+
}
490+
482491
irType = GetIRType (context, type, out size, out isPointer);
483492
typeInfo = new LlvmTypeInfo (
484493
isPointer: isPointer,
@@ -490,6 +499,21 @@ void WriteType (GeneratorWriteContext context, Type type, object? value, out Llv
490499
context.Output.Write (irType);
491500
}
492501

502+
void WriteStringBlobType (GeneratorWriteContext context, LlvmIrStringBlob? blob, out LlvmTypeInfo typeInfo)
503+
{
504+
long size = blob?.Size ?? 0;
505+
// Blobs are always arrays of bytes
506+
context.Output.Write ($"[{size} x i8]");
507+
508+
typeInfo = new LlvmTypeInfo (
509+
isPointer: false,
510+
isAggregate: true,
511+
isStructure: false,
512+
size: (ulong)size,
513+
maxFieldAlignment: 1
514+
);
515+
}
516+
493517
void WriteArrayType (GeneratorWriteContext context, Type elementType, ulong elementCount, out LlvmTypeInfo typeInfo)
494518
{
495519
WriteArrayType (context, elementType, elementCount, variable: null, out typeInfo);
@@ -769,6 +793,11 @@ public void WriteValue (GeneratorWriteContext context, Type type, object? value,
769793
return;
770794
}
771795

796+
if (type == typeof(LlvmIrStringBlob)) {
797+
WriteStringBlobArray (context, (LlvmIrStringBlob)value);
798+
return;
799+
}
800+
772801
if (type.IsArray) {
773802
if (type == typeof(byte[])) {
774803
WriteInlineArray (context, (byte[])value, encodeAsASCII: true);
@@ -786,6 +815,69 @@ public void WriteValue (GeneratorWriteContext context, Type type, object? value,
786815
throw new NotSupportedException ($"Internal error: value type '{type}' is unsupported");
787816
}
788817

818+
void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob)
819+
{
820+
const uint stride = 16;
821+
Type elementType = typeof(byte);
822+
823+
LlvmIrVariableNumberFormat oldNumberFormat = context.NumberFormat;
824+
context.NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal;
825+
WriteArrayValueStart (context);
826+
foreach (LlvmIrStringBlob.StringInfo si in blob.GetSegments ()) {
827+
if (si.Offset > 0) {
828+
context.Output.Write (',');
829+
context.Output.WriteLine ();
830+
context.Output.WriteLine ();
831+
}
832+
833+
context.Output.Write (context.CurrentIndent);
834+
WriteCommentLine (context, $" '{si.Value}' @ {si.Offset}");
835+
WriteBytes (si.Bytes);
836+
}
837+
context.Output.WriteLine ();
838+
WriteArrayValueEnd (context);
839+
context.NumberFormat = oldNumberFormat;
840+
841+
void WriteBytes (byte[] bytes)
842+
{
843+
ulong counter = 0;
844+
bool first = true;
845+
foreach (byte b in bytes) {
846+
if (!first) {
847+
WriteCommaWithStride (counter);
848+
} else {
849+
context.Output.Write (context.CurrentIndent);
850+
first = false;
851+
}
852+
853+
counter++;
854+
WriteByteTypeAndValue (b);
855+
}
856+
857+
WriteCommaWithStride (counter);
858+
WriteByteTypeAndValue (0); // Terminating NUL is counted for each string, but not included in its bytes
859+
}
860+
861+
void WriteCommaWithStride (ulong counter)
862+
{
863+
context.Output.Write (',');
864+
if (stride == 1 || counter % stride == 0) {
865+
context.Output.WriteLine ();
866+
context.Output.Write (context.CurrentIndent);
867+
} else {
868+
context.Output.Write (' ');
869+
}
870+
}
871+
872+
void WriteByteTypeAndValue (byte v)
873+
{
874+
WriteType (context, elementType, v, out _);
875+
876+
context.Output.Write (' ');
877+
WriteValue (context, elementType, v);
878+
}
879+
}
880+
789881
void WriteStructureValue (GeneratorWriteContext context, StructureInstance? instance)
790882
{
791883
if (instance == null || instance.IsZeroInitialized) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Xamarin.Android.Tasks.LLVMIR;
5+
6+
/// <summary><para>
7+
/// This class is an optimization which allows us to store strings
8+
/// as a single "blob" of data where each string follows another, all
9+
/// of them separated with a NUL character. This allows us to use a single
10+
/// pointer at run time instead of several (one per string). The result is
11+
/// less relocations in the final .so, which is good for performance
12+
/// </para><para>
13+
/// Each string is converted to UTF8 before storing as a byte array. To optimize
14+
/// for size, duplicate strings are not stored, instead the earlier offset+length
15+
/// are returned when calling the <see cref="Add(string)" /> method.
16+
/// </para>
17+
/// </summary>
18+
class LlvmIrStringBlob
19+
{
20+
// Length is one more than byte size, to account for the terminating nul
21+
public record struct StringInfo (int Offset, int Length, byte[] Bytes, string Value);
22+
23+
Dictionary<string, StringInfo> cache = new (StringComparer.Ordinal);
24+
List<StringInfo> segments = new ();
25+
long size = 0;
26+
27+
public long Size => size;
28+
29+
public (int offset, int length) Add (string s)
30+
{
31+
if (cache.TryGetValue (s, out StringInfo info)) {
32+
return (info.Offset, info.Length);
33+
}
34+
35+
byte[] bytes = MonoAndroidHelper.Utf8StringToBytes (s);
36+
int offset;
37+
if (segments.Count > 0) {
38+
StringInfo lastSegment = segments[segments.Count - 1];
39+
offset = lastSegment.Offset + lastSegment.Length + 1; // Include trailing NUL here
40+
} else {
41+
offset = 0;
42+
}
43+
44+
info = new StringInfo (
45+
Offset: offset,
46+
Length: bytes.Length,
47+
Bytes: bytes,
48+
Value: s
49+
);
50+
segments.Add (info);
51+
cache.Add (s, info);
52+
size += info.Length + 1; // Account for the trailing NUL
53+
54+
return (info.Offset, info.Length);
55+
}
56+
57+
public int GetIndexOf (string s)
58+
{
59+
if (String.IsNullOrEmpty (s)) {
60+
return -1;
61+
}
62+
63+
if (!cache.TryGetValue (s, out StringInfo info)) {
64+
return -1;
65+
}
66+
67+
return info.Offset;
68+
}
69+
70+
public IEnumerable<StringInfo> GetSegments ()
71+
{
72+
foreach (StringInfo si in segments) {
73+
yield return si;
74+
}
75+
}
76+
}

src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,10 @@ static TypeMapDebugEntry GetDebugEntry (TypeDefinition td, TypeDefinitionCache c
145145
return new TypeMapDebugEntry {
146146
JavaName = Java.Interop.Tools.TypeNameMappings.JavaNativeTypeManager.ToJniName (td, cache),
147147
ManagedName = GetManagedTypeName (td),
148+
ManagedTypeTokenId = td.MetadataToken.ToUInt32 (),
148149
TypeDefinition = td,
149150
SkipInJavaToManaged = ShouldSkipInJavaToManaged (td),
151+
AssemblyName = td.Module.Assembly.Name.Name,
150152
};
151153
}
152154

src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ internal sealed class TypeMapDebugEntry
6262
{
6363
public string JavaName;
6464
public string ManagedName;
65+
public uint ManagedTypeTokenId;
6566
public bool SkipInJavaToManaged;
6667
public TypeMapDebugEntry DuplicateForJavaToManaged;
68+
public string AssemblyName;
6769

6870
// This field is only used by the Cecil adapter for temp storage while reading.
6971
// It is not used to create the typemap.

src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.IO;
45
using System.Linq;
56
using System.Xml;
@@ -101,6 +102,7 @@ void WriteTypeMapDebugEntry (XmlWriter xml, TypeMapDebugEntry entry)
101102
xml.WriteAttributeStringIfNotDefault ("managed-name", entry.ManagedName);
102103
xml.WriteAttributeStringIfNotDefault ("skip-in-java-to-managed", entry.SkipInJavaToManaged);
103104
xml.WriteAttributeStringIfNotDefault ("is-invoker", entry.IsInvoker);
105+
xml.WriteAttributeString ("managed-type-token-id", entry.ManagedTypeTokenId.ToString (CultureInfo.InvariantCulture));
104106
xml.WriteEndElement ();
105107
}
106108

@@ -198,19 +200,20 @@ public static TypeMapObjectsXmlFile Import (string filename)
198200

199201
static void ImportDebugData (XElement root, TypeMapObjectsXmlFile file)
200202
{
201-
var isMonoAndroid = root.GetAttributeOrDefault ("assembly-name", string.Empty) == "Mono.Android";
203+
var assemblyName = root.GetAttributeOrDefault ("assembly-name", string.Empty);
204+
var isMonoAndroid = assemblyName == "Mono.Android";
202205
var javaToManaged = root.Element ("java-to-managed");
203206

204207
if (javaToManaged is not null) {
205208
foreach (var entry in javaToManaged.Elements ("entry"))
206-
file.JavaToManagedDebugEntries.Add (FromDebugEntryXml (entry, isMonoAndroid));
209+
file.JavaToManagedDebugEntries.Add (FromDebugEntryXml (entry, assemblyName, isMonoAndroid));
207210
}
208211

209212
var managedToJava = root.Element ("managed-to-java");
210213

211214
if (managedToJava is not null) {
212215
foreach (var entry in managedToJava.Elements ("entry"))
213-
file.ManagedToJavaDebugEntries.Add (FromDebugEntryXml (entry, isMonoAndroid));
216+
file.ManagedToJavaDebugEntries.Add (FromDebugEntryXml (entry, assemblyName, isMonoAndroid));
214217
}
215218
}
216219

@@ -251,19 +254,22 @@ public static void WriteEmptyFile (string destination, TaskLoggingHelper log)
251254
File.Create (destination).Dispose ();
252255
}
253256

254-
static TypeMapDebugEntry FromDebugEntryXml (XElement entry, bool isMonoAndroid)
257+
static TypeMapDebugEntry FromDebugEntryXml (XElement entry, string assemblyName, bool isMonoAndroid)
255258
{
256259
var javaName = entry.GetAttributeOrDefault ("java-name", string.Empty);
257260
var managedName = entry.GetAttributeOrDefault ("managed-name", string.Empty);
258261
var skipInJavaToManaged = entry.GetAttributeOrDefault ("skip-in-java-to-managed", false);
259262
var isInvoker = entry.GetAttributeOrDefault ("is-invoker", false);
263+
var managedTokenId = entry.GetAttributeOrDefault ("managed-type-token-id", (uint)0);
260264

261265
return new TypeMapDebugEntry {
262266
JavaName = javaName,
263267
ManagedName = managedName,
268+
ManagedTypeTokenId = managedTokenId,
264269
SkipInJavaToManaged = skipInJavaToManaged,
265270
IsInvoker = isInvoker,
266271
IsMonoAndroid = isMonoAndroid,
272+
AssemblyName = assemblyName,
267273
};
268274
}
269275

src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ public override string GetComment (object data, string fieldName)
6666
var entry = EnsureType<TypeMapEntry> (data);
6767

6868
if (String.Compare ("from", fieldName, StringComparison.Ordinal) == 0) {
69-
return $"from: {entry.from}";
69+
return $" from: {entry.from}";
7070
}
7171

7272
if (String.Compare ("to", fieldName, StringComparison.Ordinal) == 0) {
73-
return $"to: {entry.to}";
73+
return $" to: {entry.to}";
7474
}
7575

7676
return String.Empty;

0 commit comments

Comments
 (0)