Skip to content

Commit

Permalink
[wip] p/invoke preservation source code generator
Browse files Browse the repository at this point in the history
  • Loading branch information
grendello committed Jul 3, 2024
1 parent 962a780 commit 7f5c98e
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 21 deletions.
10 changes: 5 additions & 5 deletions src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,23 @@ void Run (bool useMarshalMethods)
var nativeCodeGenStates = new Dictionary<AndroidTargetArch, NativeCodeGenState> ();
bool generateJavaCode = true;
NativeCodeGenState? templateCodeGenState = null;
var scanner = new PinvokeScanner (Log);
PinvokeScanner? pinvokeScanner = EnableNativeRuntimeLinking ? new PinvokeScanner (Log) : null;

foreach (var kvp in allAssembliesPerArch) {
AndroidTargetArch arch = kvp.Key;
Dictionary<string, ITaskItem> archAssemblies = kvp.Value;
(bool success, NativeCodeGenState? state) = GenerateJavaSourcesAndMaybeClassifyMarshalMethods (arch, archAssemblies, MaybeGetArchAssemblies (userAssembliesPerArch, arch), useMarshalMethods, generateJavaCode);

if (!success) {
if (!success || state == null) {
return;
}

if (EnableNativeRuntimeLinking) {
(success, List<PinvokeScanner.PinvokeEntryInfo> pinfos) = ScanForUsedPinvokes (scanner, arch, state.Resolver);
if (pinvokeScanner != null) {
(success, List<PinvokeScanner.PinvokeEntryInfo> pinfos) = ScanForUsedPinvokes (pinvokeScanner, arch, state.Resolver);
if (!success) {
return;
}
BuildEngine4.RegisterTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (PinvokeScanner.PinvokesInfoRegisterTaskKey), pinfos, RegisteredTaskObjectLifetime.Build);
state.PinvokeInfos = pinfos;
}

if (generateJavaCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ void AddEnvironment ()
}

Dictionary<AndroidTargetArch, NativeCodeGenState>? nativeCodeGenStates = null;
if (enableMarshalMethods) {
if (enableMarshalMethods || EnableNativeRuntimeLinking) {
nativeCodeGenStates = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<Dictionary<AndroidTargetArch, NativeCodeGenState>> (
ProjectSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateRegisterTaskKey),
RegisteredTaskObjectLifetime.Build
Expand Down Expand Up @@ -370,8 +370,10 @@ void AddEnvironment ()
string targetAbi = abi.ToLowerInvariant ();
string environmentBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"environment.{targetAbi}");
string marshalMethodsBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"marshal_methods.{targetAbi}");
string? pinvokePreserveBaseAsmFilePath = EnableNativeRuntimeLinking ? Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}") : null;
string environmentLlFilePath = $"{environmentBaseAsmFilePath}.ll";
string marshalMethodsLlFilePath = $"{marshalMethodsBaseAsmFilePath}.ll";
string? pinvokePreserveLlFilePath = pinvokePreserveBaseAsmFilePath != null ? $"{pinvokePreserveBaseAsmFilePath}.ll" : null;
AndroidTargetArch targetArch = GetAndroidTargetArchForAbi (abi);

using var appConfigWriter = MemoryStreamPool.Shared.CreateStreamWriter ();
Expand Down Expand Up @@ -402,10 +404,17 @@ void AddEnvironment ()
}

if (EnableNativeRuntimeLinking) {
// var pinfoGen = new PreservePinvokesNativeAssemblyGenerator (
// Log,
// targetArch,

var pinvokePreserveGen = new PreservePinvokesNativeAssemblyGenerator (Log, EnsureCodeGenState (targetArch), MonoComponents);
LLVMIR.LlvmIrModule pinvokePreserveModule = pinvokePreserveGen.Construct ();
using var pinvokePreserveWriter = MemoryStreamPool.Shared.CreateStreamWriter ();
try {
pinvokePreserveGen.Generate (pinvokePreserveModule, targetArch, pinvokePreserveWriter, pinvokePreserveLlFilePath);
} catch {
throw;
} finally {
pinvokePreserveWriter.Flush ();
Files.CopyIfStreamChanged (pinvokePreserveWriter.BaseStream, pinvokePreserveLlFilePath);
}
}

LLVMIR.LlvmIrModule marshalMethodsModule = marshalMethodsAsmGen.Construct ();
Expand Down
3 changes: 3 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class PrepareAbiItems : AndroidTask
const string CompressedAssembliesBase = "compressed_assemblies";
const string JniRemappingBase = "jni_remap";
const string MarshalMethodsBase = "marshal_methods";
const string PinvokePreserveBase = "pinvoke_preserve";

public override string TaskPrefix => "PAI";

Expand Down Expand Up @@ -56,6 +57,8 @@ public override bool RunTask ()
baseName = JniRemappingBase;
} else if (String.Compare ("marshal_methods", Mode, StringComparison.OrdinalIgnoreCase) == 0) {
baseName = MarshalMethodsBase;
} else if (String.Compare ("runtime_linking", Mode, StringComparison.OrdinalIgnoreCase) == 0) {
baseName = PinvokePreserveBase;
} else {
Log.LogError ($"Unknown mode: {Mode}");
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ class NativeCodeGenState
/// </summary>
public List<TypeDefinition> AllJavaTypes { get; }

/// <summary>
/// Contains information about p/invokes used by the managed assemblies included in the
/// application. Will be **null** unless native runtime linking at application build time
/// is enabled.
/// </summary>
public List<PinvokeScanner.PinvokeEntryInfo>? PinvokeInfos { get; set; }

public List<TypeDefinition> JavaTypesForJCW { get; }
public XAAssemblyResolver Resolver { get; }
public TypeDefinitionCache TypeCache { get; }
Expand Down
2 changes: 0 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Utilities/PinvokeScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ namespace Xamarin.Android.Tasks;

class PinvokeScanner
{
public const string PinvokesInfoRegisterTaskKey = ".:!PreservePinvokesTaskKey!:.";

public sealed class PinvokeEntryInfo
{
public readonly string LibraryName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text;

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

using Xamarin.Android.Tasks.LLVMIR;
Expand All @@ -17,19 +18,107 @@ namespace Xamarin.Android.Tasks;

class PreservePinvokesNativeAssemblyGenerator : LlvmIrComposer
{
readonly TaskLoggingHelper log;
readonly AndroidTargetArch targetArch;
readonly ICollection<PinvokeScanner.PinvokeEntryInfo> pinfos;
// Maps a component name after ridding it of the `lib` prefix and the extension to a "canonical"
// name of a library, as used in `[DllImport]` attributes.
readonly Dictionary<string, string> libraryNameMap = new (StringComparer.Ordinal) {
{ "xa-java-interop", "java-interop" },
{ "mono-android.release-static", String.Empty },
{ "mono-android.release", String.Empty },
};

public PreservePinvokesNativeAssemblyGenerator (TaskLoggingHelper log, AndroidTargetArch targetArch, ICollection<PinvokeScanner.PinvokeEntryInfo> pinfos)
readonly NativeCodeGenState state;
readonly ITaskItem[] monoComponents;

public PreservePinvokesNativeAssemblyGenerator (TaskLoggingHelper log, NativeCodeGenState codeGenState, ITaskItem[] monoComponents)
: base (log)
{
this.log = log;
this.targetArch = targetArch;
this.pinfos = pinfos;
if (codeGenState.PinvokeInfos == null) {
throw new InvalidOperationException ($"Internal error: {nameof (codeGenState)} `{nameof (codeGenState.PinvokeInfos)}` property is `null`");
}

this.state = codeGenState;
this.monoComponents = monoComponents;
}

protected override void Construct (LlvmIrModule module)
{
Log.LogDebugMessage ("Constructing p/invoke preserve code");
List<PinvokeScanner.PinvokeEntryInfo> pinvokeInfos = state.PinvokeInfos!;
if (pinvokeInfos.Count == 0) {
// This is a very unlikely scenario, but we will work just fine. The module that this generator produces will merely result
// in an empty (but valid) .ll file and an "empty" object file to link into the shared library.
return;
}

Log.LogDebugMessage (" Looking for enabled native components");
var componentNames = new List<string> ();
var nativeComponents = new NativeRuntimeComponents (monoComponents);
foreach (NativeRuntimeComponents.Archive archiveItem in nativeComponents.KnownArchives) {
if (!archiveItem.Include) {
continue;
}

Log.LogDebugMessage ($" {archiveItem.Name}");
componentNames.Add (archiveItem.Name);
}

if (componentNames.Count == 0) {
Log.LogDebugMessage ("No native framework components are included in the build, not scanning for p/invoke usage");
return;
}

Log.LogDebugMessage (" Checking discovered p/invokes against the list of components");
foreach (PinvokeScanner.PinvokeEntryInfo pinfo in pinvokeInfos) {
Log.LogDebugMessage ($" p/invoke: {pinfo.EntryName} in {pinfo.LibraryName}");
if (MustPreserve (pinfo, componentNames)) {
Log.LogDebugMessage (" must be preserved");
} else {
Log.LogDebugMessage (" no need to preserve");
}
}
}

// Returns `true` for all p/invokes that we know are part of our set of components, otherwise returns `false`.
// Returning `false` merely means that the p/invoke isn't in any of BCL or our code and therefore we shouldn't
// care. It doesn't mean the p/invoke will be removed in any way.
bool MustPreserve (PinvokeScanner.PinvokeEntryInfo pinfo, List<string> components)
{
if (String.Compare ("xa-internal-api", pinfo.LibraryName, StringComparison.Ordinal) == 0) {
return true;
}

foreach (string component in components) {
// The most common pattern for the BCL - file name without extension
string componentName = Path.GetFileNameWithoutExtension (component);
if (Matches (pinfo.LibraryName, componentName)) {
return true;
}

// If it starts with `lib`, drop the prefix
if (componentName.StartsWith ("lib", StringComparison.Ordinal)) {
if (Matches (pinfo.LibraryName, componentName.Substring (3))) {
return true;
}
}

// Might require mapping of component name to a canonical one
if (libraryNameMap.TryGetValue (componentName, out string? mappedComponentName) && !String.IsNullOrEmpty (mappedComponentName)) {
if (Matches (pinfo.LibraryName, mappedComponentName)) {
return true;
}
}

// Try full file name, as the last resort
if (Matches (pinfo.LibraryName, Path.GetFileName (component))) {
return true;
}
}

return false;

bool Matches (string libraryName, string componentName)
{
return String.Compare (libraryName, componentName, StringComparison.Ordinal) == 0;
}
}
}
18 changes: 16 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,15 @@ because xbuild doesn't support framework reference assemblies.
Mode="marshal_methods">
<Output TaskParameter="AssemblySources" ItemName="_MarshalMethodsAssemblySource" />
</PrepareAbiItems>
<PrepareAbiItems
Condition=" '$(_AndroidEnableNativeRuntimeLinking)' == 'true' "
BuildTargetAbis="@(_BuildTargetAbis)"
NativeSourcesDir="$(_NativeAssemblySourceDir)"
InstantRunEnabled="$(_InstantRunEnabled)"
Debug="$(AndroidIncludeDebugSymbols)"
Mode="runtime_linking">
<Output TaskParameter="AssemblySources" ItemName="_RuntimeLinkingAssemblySource" />
</PrepareAbiItems>
</Target>

<Target Name="_GenerateEnvironmentFiles" DependsOnTargets="_ReadAndroidManifest">
Expand Down Expand Up @@ -1740,6 +1749,8 @@ because xbuild doesn't support framework reference assemblies.
<ItemGroup>
<FileWrites Include="$(_AndroidBuildIdFile)" />
<FileWrites Include="@(_EnvironmentAssemblySource)" />
<FileWrites Include="@(_MarshalMethodsAssemblySource)" />
<FileWrites Condition=" '$(_AndroidEnableNativeRuntimeLinking)' == 'true' " Include="@(_RuntimeLinkingAssemblySource)" />
</ItemGroup>
</Target>

Expand Down Expand Up @@ -1964,6 +1975,9 @@ because xbuild doesn't support framework reference assemblies.
<_NativeAssemblyTarget Include="@(_AndroidRemapAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')">
<abi>%(_AndroidRemapAssemblySource.abi)</abi>
</_NativeAssemblyTarget>
<_NativeAssemblyTarget Include="@(_RuntimeLinkingAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')">
<abi>%(_RuntimeLinkingAssemblySource.abi)</abi>
</_NativeAssemblyTarget>
</ItemGroup>
</Target>

Expand All @@ -1983,10 +1997,10 @@ because xbuild doesn't support framework reference assemblies.

<Target Name="_CompileNativeAssemblySources"
DependsOnTargets="_PrepareNativeAssemblyItems;_GenerateCompressedAssembliesNativeSourceFiles"
Inputs="@(_TypeMapAssemblySource);@(_TypeMapAssemblyInclude);@(_EnvironmentAssemblySource);@(_CompressedAssembliesAssemblySource);@(_MarshalMethodsAssemblySource);@(_AndroidRemapAssemblySource)"
Inputs="@(_TypeMapAssemblySource);@(_TypeMapAssemblyInclude);@(_EnvironmentAssemblySource);@(_CompressedAssembliesAssemblySource);@(_MarshalMethodsAssemblySource);@(_AndroidRemapAssemblySource);@(_RuntimeLinkingAssemblySource)"
Outputs="@(_NativeAssemblyTarget)">
<CompileNativeAssembly
Sources="@(_TypeMapAssemblySource);@(_EnvironmentAssemblySource);@(_CompressedAssembliesAssemblySource);@(_MarshalMethodsAssemblySource);@(_AndroidRemapAssemblySource)"
Sources="@(_TypeMapAssemblySource);@(_EnvironmentAssemblySource);@(_CompressedAssembliesAssemblySource);@(_MarshalMethodsAssemblySource);@(_AndroidRemapAssemblySource);@(_RuntimeLinkingAssemblySource)"
DebugBuild="$(AndroidIncludeDebugSymbols)"
WorkingDirectory="$(_NativeAssemblySourceDir)"
AndroidBinUtilsDirectory="$(AndroidBinUtilsDirectory)"
Expand Down

0 comments on commit 7f5c98e

Please sign in to comment.