Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
extern alias xamarinbuildtasks;

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Android.Sdk.TrimmableTypeMap;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NUnit.Framework;
using FieldRvaTable = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.FieldRvaTable;
using JniAssemblyRewriter = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniAssemblyRewriter;
using JniRewriteResult = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniRewriteResult;
using R8Mapping = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.R8Mapping;

namespace Xamarin.Android.Build.Tests;

[TestFixture]
[Parallelizable (ParallelScope.Children)]
public class JniAssemblyRewriterTypeMapTests
{
[Test]
public void RewritesGeneratedTypeMapWithOwnerSpecificMethodNames ()
{
byte [] source = GenerateTypeMapWithSharedMethodName ();
var warnings = new List<BuildWarningEventArgs> ();

JniRewriteResult result = Rewrite (source, Mapping (
"test.First -> a.b.First:\n" +
" void n_Run() -> a\n" +
"test.Second -> a.b.Second:\n" +
" void n_Run() -> b\n"), warnings);

CollectionAssert.AreEquivalent (new [] { "a", "b", "()V" }, ReadUtf8Values (result.Image));
CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326");
}

[Test]
public void RewritesGeneratedTypeMapWithMappedAndUnmappedMethodNames ()
{
byte [] source = GenerateTypeMapWithSharedMethodName ();
var warnings = new List<BuildWarningEventArgs> ();

JniRewriteResult result = Rewrite (source, Mapping (
"test.First -> a.b.First:\n" +
" void n_Run() -> a\n" +
"test.Second -> test.Second:\n"), warnings);

CollectionAssert.AreEquivalent (new [] { "a", "n_Run", "()V" }, ReadUtf8Values (result.Image));
CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326");
}

static JniRewriteResult Rewrite (byte [] sourceImage, R8Mapping mapping, IList<BuildWarningEventArgs> warnings)
{
var engine = new MockBuildEngine (TestContext.Out, warnings: warnings);
var log = new TaskLoggingHelper (engine, nameof (JniAssemblyRewriterTypeMapTests));
return JniAssemblyRewriter.Rewrite (sourceImage, mapping, log);
}

static R8Mapping Mapping (string text) => R8Mapping.Parse (new StringReader (text));

static byte [] GenerateTypeMapWithSharedMethodName ()
{
var peers = new [] {
CreatePeer ("test/First", "Test.First"),
CreatePeer ("test/Second", "Test.Second"),
};
using var stream = new MemoryStream ();
new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)).Generate (peers, stream, "OwnerSpecificNames");
return stream.ToArray ();
}

static JavaPeerInfo CreatePeer (string javaName, string managedName)
{
int separator = managedName.LastIndexOf ('.');
return new JavaPeerInfo {
JavaName = javaName,
CompatJniName = javaName,
ManagedTypeName = managedName,
ManagedTypeNamespace = managedName.Substring (0, separator),
ManagedTypeShortName = managedName.Substring (separator + 1),
AssemblyName = "TestAsm",
DoNotGenerateAcw = false,
ActivationCtor = new ActivationCtorInfo {
DeclaringTypeName = managedName,
DeclaringAssemblyName = "TestAsm",
Style = ActivationCtorStyle.XamarinAndroid,
},
MarshalMethods = [
new MarshalMethodInfo {
JniName = "run",
NativeCallbackName = "n_Run",
JniSignature = "()V",
ManagedMethodName = "Run",
},
],
};
}

static string [] ReadUtf8Values (byte [] image)
{
using var peReader = new PEReader (ImmutableArray.Create (image));
MetadataReader reader = peReader.GetMetadataReader ();
return FieldRvaTable.Read (peReader, reader).Entries
.Select (entry => entry.Utf8Value)
.OfType<string> ()
.ToArray ();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Android.Build.Tasks.csproj" />
<ProjectReference Include="..\..\..\Xamarin.Android.Build.Tasks\Xamarin.Android.Build.Tasks.csproj">
<Aliases>xamarinbuildtasks</Aliases>
</ProjectReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

/// <summary>
/// A read-only, seekable <see cref="Stream"/> over the chunks a <see cref="BlobBuilder"/> already
/// holds, so a serialised PE image can be hashed and copied to disk without being duplicated into
/// a second contiguous buffer.
/// </summary>
/// <remarks>
/// Only the chunk arrays are retained — the <see cref="BlobBuilder"/> itself and the metadata
/// graph that produced it stay collectible — so the live byte count matches what a
/// <see cref="MemoryStream"/> copy would have held, without the transient second copy.
/// The source builder must not be cleared, freed, or reused after this stream is created because
/// the stream retains references to its chunk arrays.
/// </remarks>
sealed class BlobBuilderStream : Stream
{
readonly ArraySegment<byte> [] segments;
readonly long [] segmentStarts;
readonly long length;
long position;
int cursor;

public BlobBuilderStream (BlobBuilder builder)
{
_ = builder ?? throw new ArgumentNullException (nameof (builder));

var collected = new List<ArraySegment<byte>> ();
foreach (var blob in builder.GetBlobs ()) {
var bytes = blob.GetBytes ();
if (bytes.Count == 0 || bytes.Array is null) {
continue;
}
collected.Add (bytes);
}

segments = collected.ToArray ();
segmentStarts = new long [segments.Length + 1];
long total = 0;
for (int i = 0; i < segments.Length; i++) {
segmentStarts [i] = total;
total += segments [i].Count;
}
segmentStarts [segments.Length] = total;
length = total;
}

public override bool CanRead => true;

public override bool CanSeek => true;

public override bool CanWrite => false;

public override long Length => length;

public override long Position {
get => position;
set {
if (value < 0) {
throw new ArgumentOutOfRangeException (nameof (value));
}
position = value;
}
}

public override void Flush ()
{
}

public override int Read (byte [] buffer, int offset, int count)
{
if (buffer is null) {
throw new ArgumentNullException (nameof (buffer));
}
if (offset < 0) {
throw new ArgumentOutOfRangeException (nameof (offset));
}
if (count < 0) {
throw new ArgumentOutOfRangeException (nameof (count));
}
if (buffer.Length - offset < count) {
throw new ArgumentException ("The buffer is too small for the requested range.");
}

int copied = 0;
while (count > 0 && position < length) {
int index = FindSegment (position);
var segment = segments [index];
int within = (int) (position - segmentStarts [index]);
int available = segment.Count - within;
int toCopy = Math.Min (available, count);
var source = segment.Array ?? throw new InvalidOperationException ("Blob segment has no backing array.");
Buffer.BlockCopy (source, segment.Offset + within, buffer, offset, toCopy);
position += toCopy;
offset += toCopy;
count -= toCopy;
copied += toCopy;
}
return copied;
}

public override long Seek (long offset, SeekOrigin origin)
{
long target = origin switch {
SeekOrigin.Begin => offset,
SeekOrigin.Current => position + offset,
SeekOrigin.End => length + offset,
_ => throw new ArgumentOutOfRangeException (nameof (origin)),
};
if (target < 0) {
throw new IOException ("Cannot seek before the beginning of the stream.");
}
position = target;
return position;
}

public override void SetLength (long value) => throw new NotSupportedException ();

public override void Write (byte [] buffer, int offset, int count) => throw new NotSupportedException ();

int FindSegment (long offset)
{
// Reads are overwhelmingly sequential, so try the last used chunk first.
Comment thread
simonrozsival marked this conversation as resolved.
if (cursor < segments.Length && offset >= segmentStarts [cursor] && offset < segmentStarts [cursor + 1]) {
return cursor;
}

int low = 0;
int high = segments.Length - 1;
while (low <= high) {
int middle = low + ((high - low) / 2);
if (offset < segmentStarts [middle]) {
high = middle - 1;
} else if (offset >= segmentStarts [middle + 1]) {
low = middle + 1;
} else {
cursor = middle;
return middle;
}
}
throw new ArgumentOutOfRangeException (nameof (offset));
}
}
Loading
Loading