Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 57 additions & 0 deletions docs/code/class-handles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Objective-C classes

Objective-C classes can be referenced from managed code in several ways:

* Calls to Class.GetHandle / GetHandleIntrinsic

It's highly desirable to use a direct native reference to Objective-C classes when building a mobile app, for a few reasons:

* It's faster at runtime, and the app is smaller.
* If the referenced Objective-C class comes from a third-party static library, the
native linker can remove it if it's configured to remove unused code
(because the native linker can't see that the class is in fact used
at runtime) unless there's a direct native reference to the class.

On the other hand there's one scenario when a direct native reference is not desirable: when the native Objective-C class does not exist.

In order to create a direct native reference to Objective-C classes, we need to know the names of those Objective-C classes.

## The `InlineClassGetHandle` property

This behavior is controlled by the `InlineClassGetHandle` MSBuild property, which
can either be enabled or disabled.

See the [build properties documentation](../building-apps/build-properties.md) for default values.

## How it works

During the build we try to collect the following:

* Any calls to `Class.GetHandle[Intrinsic]` APIs: we try to collect the class name (this might not always succeed, if the class name is not a constant).

This is further complicated by the fact that we only want to create native
references for managed references that survive trimming.

So we do the following:

1. During trimming, two custom linker steps execute:

* `InlineClassGetHandleStep`: for every call `Class.GetHandle` we've
collected, this step creates a P/Invoke to a native method that will
return the Objective-C class for that symbol (using a direct native
reference), and modifies the code that fetches that symbol to call said
P/Invoke.

2. After trimming, we figure out which of those symbols survived:

* For ILTrim: the `_CollectPostILTrimInformation` MSBuild target inspects
the trimmed assemblies and collects all the inlined P/Invokes that
survived. Per-assembly results are cached to speed up incremental builds.
* For NativeAOT: the `_CollectPostNativeAOTTrimInformation` MSBuild target
inspects the native object file (or static library) produced by NativeAOT,
collects all unresolved native references, and filters them against the
Objective-C classes to determine which survived.

3. The `_PostTrimmingProcessing` MSBuild target takes the surviving symbols
from either path, generates the corresponding native Objective-C code, and
adds it to the list of files to compile and link into the final executable.
6 changes: 6 additions & 0 deletions dotnet/targets/Xamarin.Shared.Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@
<InlineDlfcnMethods Condition="'$(InlineDlfcnMethods)' == ''">compatibility</InlineDlfcnMethods>
</PropertyGroup>

<!-- Set default value for InlineClassGetHandle based on .NET version -->
<PropertyGroup Condition="'$(InlineClassGetHandle)' == '' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '11.0'))">
Comment thread
rolfbjarne marked this conversation as resolved.
<InlineClassGetHandle Condition="'$(_UseNativeAot)' == 'true'">strict</InlineClassGetHandle>
<InlineClassGetHandle Condition="'$(InlineClassGetHandle)' == ''">compatibility</InlineClassGetHandle>
</PropertyGroup>

<!-- Set the default RuntimeIdentifier if not already specified. -->
<PropertyGroup Condition="'$(_RuntimeIdentifierIsRequired)' == 'true' And '$(RuntimeIdentifier)' == '' And '$(RuntimeIdentifiers)' == '' ">
<!-- The _<platform>RuntimeIdentifier values are set from the IDE -->
Expand Down
27 changes: 22 additions & 5 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@
@(_BundlerEnvironmentVariables -> 'EnvironmentVariable=Overwrite=%(Overwrite)|%(Identity)=%(Value)')
@(_XamarinFrameworkAssemblies -> 'FrameworkAssembly=%(Filename)')
Interpreter=$(MtouchInterpreter)
InlineClassGetHandle=$(InlineClassGetHandle)
InlineDlfcnMethods=$(InlineDlfcnMethods)
IntermediateLinkDir=$(IntermediateLinkDir)
IntermediateOutputPath=$(DeviceSpecificIntermediateOutputPath)
Expand Down Expand Up @@ -674,6 +675,7 @@
TargetArchitectures=$(TargetArchitectures)
TargetFramework=$(_ComputedTargetFrameworkMoniker)
TypeMapAssemblyName=$(_TypeMapAssemblyName)
TypeMapFilePath=$(_TypeMapFilePath)
TypeMapOutputDirectory=$(_TypeMapOutputDirectory)
UseLlvm=$(MtouchUseLlvm)
Verbosity=$(_BundlerVerbosity)
Expand Down Expand Up @@ -798,6 +800,7 @@
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreMarkDispatcher" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static'" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(Registrar)' == 'trimmable-static'" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineClassGetHandleStep" Condition="'$(InlineClassGetHandle)' != ''" />

<!--
IMarkHandlers which run during Mark
Expand Down Expand Up @@ -826,9 +829,9 @@
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.LoadNonSkippedAssembliesStep" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.ExtractBindingLibrariesStep" />
<!-- The ListExportedSymbols must run after ExtractBindingLibrariesStep, otherwise we won't properly list exported Objective-C classes from binding libraries -->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.Steps.ListExportedSymbols" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.Steps.ListExportedSymbols" Condition="'$(InlineClassGetHandle)' == '' Or '$(InlineDlfcnMethods)' == ''"/>
Comment thread
rolfbjarne marked this conversation as resolved.
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.Steps.PreOutputDispatcher" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.ClassHandleRewriterStep" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="OutputStep" Type="Xamarin.Linker.ClassHandleRewriterStep" Condition="'$(InlineClassGetHandle)' == ''" />

<!--
post-output steps
Expand Down Expand Up @@ -1322,6 +1325,9 @@
<_EmbeddedFrameworksRPath Condition="'$(SdkIsDesktop)' == 'true'">@executable_path/../Frameworks/</_EmbeddedFrameworksRPath>

<_RuntimeConfigurationFile>runtimeconfig.bin</_RuntimeConfigurationFile>

<!-- Path to the file where Objective-C type map information is stored -->
<_TypeMapFilePath Condition="'$(_TypeMapFilePath)' == ''">$(DeviceSpecificIntermediateOutputPath)type-map.txt</_TypeMapFilePath>
</PropertyGroup>

<!-- Not sure about how to handle nested app extensions here, but if it ever becomes a problem we can look into it (I believe only watch extensions can have embedded extensions at this point, and we don't support watchOS on .NET anyways) -->
Expand Down Expand Up @@ -1688,20 +1694,22 @@
<Target Name="_ComputePostTrimmingPaths">
<PropertyGroup>
<_ILTrimSurvivingNativeSymbolsFile>$(DeviceSpecificIntermediateOutputPath)inlined-dlfcn\iltrim-surviving-native-symbols.txt</_ILTrimSurvivingNativeSymbolsFile>
<_NativeAOTUnresolvedSymbolsFile>$(DeviceSpecificIntermediateOutputPath)nativeaot-unresolved-symbols.txt</_NativeAOTUnresolvedSymbolsFile>
<_NativeAOTSurvivingNativeSymbolsFile>$(DeviceSpecificIntermediateOutputPath)nativeaot-surviving-native-symbols.txt</_NativeAOTSurvivingNativeSymbolsFile>
<_ILTrimSurvivingClassesFile>$(DeviceSpecificIntermediateOutputPath)inlined-class-gethandle\iltrim-classes.txt</_ILTrimSurvivingClassesFile>
<_NativeAOTSurvivingClassesFile>$(DeviceSpecificIntermediateOutputPath)inlined-class-gethandle\nativeaot-classes.txt</_NativeAOTSurvivingClassesFile>
</PropertyGroup>
</Target>

<!-- See docs/code/native-symbols.md for an overview of native symbol handling. -->
<Target Name="_CollectPostILTrimInformation"
Condition="'$(InlineDlfcnMethods)' != '' And '$(_UseNativeAot)' != 'true'"
Condition="('$(InlineDlfcnMethods)' != '' Or '$(InlineClassGetHandle)' != '') And '$(_UseNativeAot)' != 'true'"
DependsOnTargets="_ComputeTrimmedAssemblies;_ComputePostTrimmingPaths"
Inputs="@(_TrimmedAssembly)"
Outputs="$(_ILTrimSurvivingNativeSymbolsFile)"
>
<CollectPostILTrimInformation
TrimmedAssemblies="@(_TrimmedAssembly)"
SurvivingClassesFile="$(_ILTrimSurvivingClassesFile)"
SurvivingNativeSymbolsFile="$(_ILTrimSurvivingNativeSymbolsFile)"
CacheDirectory="$(DeviceSpecificIntermediateOutputPath)posttrim-info\cache"
/>
Expand All @@ -1720,12 +1728,17 @@
<ItemGroup>
<_SurvivingNativeSymbolsFile Include="$(_ILTrimSurvivingNativeSymbolsFile)" Condition="Exists('$(_ILTrimSurvivingNativeSymbolsFile)')" />
<_SurvivingNativeSymbolsFile Include="$(_NativeAOTSurvivingNativeSymbolsFile)" Condition="Exists('$(_NativeAOTSurvivingNativeSymbolsFile)')" />

<_SurvivingClassesFiles Include="$(_ILTrimSurvivingClassesFile)" Condition="Exists('$(_ILTrimSurvivingClassesFile)')" />
<_SurvivingClassesFiles Include="$(_NativeAOTSurvivingClassesFile)" Condition="Exists('$(_NativeAOTSurvivingClassesFile)')" />
</ItemGroup>
<PostTrimmingProcessing
Architecture="$(TargetArchitectures)"
OutputDirectory="$(DeviceSpecificIntermediateOutputPath)inlined-dlfcn"
ReferenceNativeSymbol="@(ReferenceNativeSymbol)"
SurvivingClassesFiles="@(_SurvivingClassesFiles)"
SurvivingNativeSymbolsFiles="@(_SurvivingNativeSymbolsFile)"
TypeMapFilePath="$(_TypeMapFilePath)"
>
<Output TaskParameter="NativeSourceFiles" ItemName="_PostTrimmingSourceFiles" />
</PostTrimmingProcessing>
Expand Down Expand Up @@ -1829,15 +1842,19 @@
Inputs="$(NativeObject)"
Outputs="$(_NativeAOTSurvivingNativeSymbolsFile)"
>
<PropertyGroup>
<_NativeAOTUnresolvedSymbolsFile>$(DeviceSpecificIntermediateOutputPath)nativeaot-unresolved-symbols.txt</_NativeAOTUnresolvedSymbolsFile>
</PropertyGroup>
<CollectUnresolvedNativeSymbols
SessionId="$(BuildSessionId)"
StaticLibrary="$(NativeObject)"
OutputFile="$(_NativeAOTUnresolvedSymbolsFile)"
/>
<ComputeNativeAOTSurvivingNativeSymbols
SessionId="$(BuildSessionId)"
UnresolvedSymbolsFile="$(_NativeAOTUnresolvedSymbolsFile)"
SurvivingClassesFile="$(_NativeAOTSurvivingClassesFile)"
SurvivingNativeSymbolsFile="$(_NativeAOTSurvivingNativeSymbolsFile)"
UnresolvedSymbolsFile="$(_NativeAOTUnresolvedSymbolsFile)"
/>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

using Mono.Cecil;

using Xamarin.Utils;

#nullable enable

namespace Xamarin.MacDev.Tasks {
/// <summary>
/// Scans trimmed assemblies to collect information that survived trimming.
/// See docs/code/native-symbols.md for an overview of native symbol handling.
/// See docs/code/native-symbols.md and docs/code/class-handles.md for an overview of native symbol handling.
/// </summary>
public class CollectPostILTrimInformation : XamarinTask {
[Required]
Expand All @@ -26,6 +28,12 @@ public class CollectPostILTrimInformation : XamarinTask {
[Required]
public string SurvivingNativeSymbolsFile { get; set; } = "";

/// <summary>
/// Output file listing the Class.GetHandle calls that survived trimming.
/// </summary>
[Required]
public string SurvivingClassesFile { get; set; } = "";

/// <summary>
/// Directory for per-assembly cache files, to avoid re-scanning unchanged assemblies.
/// </summary>
Expand All @@ -51,17 +59,17 @@ void CollectSurvivingNativeSymbols ()
continue;

var assemblyName = Path.GetFileNameWithoutExtension (assemblyPath);
var cacheFile = Path.Combine (CacheDirectory, assemblyName + ".dlfcn-symbols.cache");
var cacheFile = Path.Combine (CacheDirectory, assemblyName + ".internal-symbols.cache");

string []? cachedSymbols = null;
if (File.Exists (cacheFile) && File.GetLastWriteTimeUtc (cacheFile) >= File.GetLastWriteTimeUtc (assemblyPath)) {
cachedSymbols = File.ReadAllLines (cacheFile);
Log.LogMessage (MessageImportance.Low, "Using cached dlfcn symbols for {0}", assemblyName);
Log.LogMessage (MessageImportance.Low, "Using cached internal symbols for {0}", assemblyName);

survivingSymbols.UnionWith (cachedSymbols);
} else {
var assemblySymbols = new HashSet<string> ();
CollectDlfcnSymbolsFromAssembly (assemblyPath, assemblySymbols);
CollectInternalSymbolsFromAssembly (assemblyPath, assemblySymbols);

// Write per-assembly cache (sorted for stability).
var sortedAssemblySymbols = assemblySymbols.OrderBy (s => s).ToArray ();
Expand All @@ -71,29 +79,53 @@ void CollectSurvivingNativeSymbols ()
}
}

WriteSymbolsToFile (this, SurvivingNativeSymbolsFile, FilterToDlfcnSymbols (survivingSymbols));
WriteSymbolsToFile (this, SurvivingClassesFile, FilterToClassSymbols (survivingSymbols));
}

public static void WriteSymbolsToFile (XamarinTask task, string file, IEnumerable<string> unsortedSymbols)
{
// Write the combined results only if contents changed (sorted for stability).
var sorted = survivingSymbols.OrderBy (s => s).ToArray ();
var sorted = unsortedSymbols.OrderBy (s => s).ToArray ();

if (File.Exists (SurvivingNativeSymbolsFile)) {
var existing = File.ReadAllLines (SurvivingNativeSymbolsFile);
if (existing.SequenceEqual (sorted))
if (File.Exists (file)) {
var existing = File.ReadAllLines (file);
if (existing.SequenceEqual (sorted)) {
task.Log.LogMessage (MessageImportance.Low, "The file {0} is already up-to-date with {1} symbols", file, sorted.Length);
return;
}
}

var dir = Path.GetDirectoryName (SurvivingNativeSymbolsFile);
if (!string.IsNullOrEmpty (dir))
Directory.CreateDirectory (dir);
File.WriteAllLines (SurvivingNativeSymbolsFile, sorted);
Log.LogMessage (MessageImportance.Low, "Found {0} surviving inlined dlfcn symbols", survivingSymbols.Count);
PathUtils.CreateDirectoryForFile (file);
File.WriteAllLines (file, sorted);
task.Log.LogMessage (MessageImportance.Low, "Wrote {0} symbols to {1}", sorted.Length, file);
}

static void CollectDlfcnSymbolsFromAssembly (string assemblyPath, HashSet<string> survivingSymbols)
public static IEnumerable<string> FilterToDlfcnSymbols (IEnumerable<string> symbols)
{
const string prefix = "xamarin_Dlfcn_";
const string suffix = "_Native";
return FilterTo (symbols, "xamarin_Dlfcn_", "_Native");
}

public static IEnumerable<string> FilterToClassSymbols (IEnumerable<string> symbols)
{
return FilterTo (symbols, "xamarin_Class_GetHandle_", "_Native");
}

static IEnumerable<string> FilterTo (IEnumerable<string> symbols, string prefix, string suffix)
{
return symbols
.Where (symbol => symbol.StartsWith (prefix, StringComparison.Ordinal) && symbol.EndsWith (suffix, StringComparison.Ordinal))
.Select (symbol => symbol.Substring (prefix.Length, symbol.Length - prefix.Length - suffix.Length));
}

static void CollectInternalSymbolsFromAssembly (string assemblyPath, HashSet<string> survivingSymbols)
{
using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, new ReaderParameters { ReadSymbols = false });
foreach (var module in assembly.Modules) {
if (!module.HasModuleReferences)
continue;
if (!module.ModuleReferences.Any (mr => mr.Name == "__Internal"))
continue;
foreach (var type in module.Types) {
if (!type.HasMethods)
continue;
Expand All @@ -102,14 +134,7 @@ static void CollectDlfcnSymbolsFromAssembly (string assemblyPath, HashSet<string
continue;
if (method.PInvokeInfo?.Module?.Name != "__Internal")
continue;
var name = method.Name;
if (!name.StartsWith (prefix) || !name.EndsWith (suffix))
continue;
var symbolLength = name.Length - prefix.Length - suffix.Length;
if (symbolLength <= 0)
continue;
var symbolName = name.Substring (prefix.Length, symbolLength);
survivingSymbols.Add (symbolName);
survivingSymbols.Add (method.PInvokeInfo.EntryPoint ?? method.Name);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#nullable enable

namespace Xamarin.MacDev.Tasks {
// See docs/code/native-symbols.md for an overview of native symbol handling.
/// See docs/code/native-symbols.md and docs/code/class-handles.md for an overview of native symbol handling.
public class CollectUnresolvedNativeSymbols : XamarinTask {
public ITaskItem? StaticLibrary { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Xamarin.MacDev.Tasks {
/// Takes the list of unresolved native symbols from a NativeAOT static library and computes
/// which inlined dlfcn native symbols survived trimming. The output file has the same format
/// as CollectPostILTrimInformation's surviving symbols file.
/// See docs/code/native-symbols.md for an overview of native symbol handling.
/// See docs/code/native-symbols.md and docs/code/class-handles.md for an overview of native symbol handling.
/// </summary>
public class ComputeNativeAOTSurvivingNativeSymbols : XamarinTask {
/// <summary>
Expand All @@ -29,39 +29,18 @@ public class ComputeNativeAOTSurvivingNativeSymbols : XamarinTask {
[Required]
public string SurvivingNativeSymbolsFile { get; set; } = "";

/// <summary>
/// Output file listing the Class.GetHandle calls that survived trimming.
/// </summary>
[Required]
public string SurvivingClassesFile { get; set; } = "";

public override bool Execute ()
{
if (!File.Exists (UnresolvedSymbolsFile))
return !Log.HasLoggedErrors;

const string prefix = "_xamarin_Dlfcn_";
const string suffix = "_Native";
var survivingSymbols = new HashSet<string> ();

foreach (var sym in File.ReadAllLines (UnresolvedSymbolsFile)) {
if (!sym.StartsWith (prefix) || !sym.EndsWith (suffix))
continue;
var symbolLength = sym.Length - prefix.Length - suffix.Length;
if (symbolLength <= 0)
continue;
var symbolName = sym.Substring (prefix.Length, symbolLength);
survivingSymbols.Add (symbolName);
}

var sorted = survivingSymbols.OrderBy (s => s).ToArray ();

if (File.Exists (SurvivingNativeSymbolsFile)) {
var existing = File.ReadAllLines (SurvivingNativeSymbolsFile);
if (existing.SequenceEqual (sorted))
return !Log.HasLoggedErrors;
}

var dir = Path.GetDirectoryName (SurvivingNativeSymbolsFile);
if (!string.IsNullOrEmpty (dir))
Directory.CreateDirectory (dir);
File.WriteAllLines (SurvivingNativeSymbolsFile, sorted);
Log.LogMessage (MessageImportance.Low, "Found {0} surviving native symbols from NativeAOT", survivingSymbols.Count);

var unresolvedSymbols = File.ReadAllLines (UnresolvedSymbolsFile);
CollectPostILTrimInformation.WriteSymbolsToFile (this, SurvivingNativeSymbolsFile, CollectPostILTrimInformation.FilterToDlfcnSymbols (unresolvedSymbols));
CollectPostILTrimInformation.WriteSymbolsToFile (this, SurvivingClassesFile, CollectPostILTrimInformation.FilterToClassSymbols (unresolvedSymbols));
return !Log.HasLoggedErrors;
}
}
Expand Down
Loading
Loading