Skip to content

Commit 8da90c2

Browse files
[TrimmableTypeMap][NativeAOT] Scan runtime host ACWs via a ref assembly
The trimmable typemap generator (_GenerateTrimmableTypeMap) runs only in the RID-independent OUTER build over @(ReferencePath), which contains just the compile closure (app + references + ref-pack framework assemblies). The NativeAOT runtime host, Microsoft.Android.Runtime.NativeAOT, is a runtime-only assembly resolved solely in the per-RID inner build (as a RID-specific runtime pack asset), so it was never scanned. Its only Java Callable Wrapper type, UncaughtExceptionMarshaler, therefore had no JCW, no typemap entry, and no acw-map entry -> R8 had nothing to keep -> an on-device startup crash in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler). Runtime-pack resolution is inherently per-RID (ResolveRuntimePackAssets needs a single RuntimeIdentifier), but the host assembly's managed metadata is RID-independent (byte-identical across RIDs). So produce a reference assembly for it and ship it in the Microsoft.Android.Sdk pack under tools/typemap-refs, then feed it to the generator as an extra framework input in the outer build. It is intentionally NOT placed in the Microsoft.Android.Ref targeting pack: files under a targeting pack's ref/ folder must all be classified in its FrameworkList and would become universal framework references for every Android app. Shipping it in the SDK pack keeps it a build-time-only input for the trimmable NativeAOT path. Its per-assembly typemap DLL (_Microsoft.Android.Runtime.NativeAOT.TypeMap) is classified as framework for ILC so it stays an IlcReference but is removed from the unmanaged-entrypoint roots, matching Mono.Android/Java.Interop; its JCW native methods register via the runtime registerNatives path. Adds Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws, which opts into the trimmable typemap on NativeAOT and asserts classes.dex retains the UncaughtExceptionMarshaler runtime ACW. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent be5272c commit 8da90c2

5 files changed

Lines changed: 97 additions & 1 deletion

File tree

build-tools/create-packs/Microsoft.Android.Sdk.proj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ core workload SDK packs imported by WorkloadManifest.targets.
8080
<FilesToPackage Include="$(MicrosoftAndroidSdkOutDir)Microsoft.Testing.Extensions.TrxReport.dll" TargetPath="tools" />
8181
<FilesToPackage Include="$(MicrosoftAndroidSdkOutDir)Microsoft.Testing.Extensions.TrxReport.Abstractions.dll" TargetPath="tools" />
8282
<FilesToPackage Include="@(VersionFiles)" TargetPath="tools" />
83+
<!-- Reference assembly for the NativeAOT runtime host. Shipped under tools/typemap-refs so the
84+
trimmable typemap generator can scan its Java Callable Wrapper types (e.g.
85+
UncaughtExceptionMarshaler) in the RID-independent outer build. It is a runtime-only
86+
assembly with no counterpart in the Microsoft.Android.Ref targeting pack. See
87+
Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets (_AddNativeAotRuntimeHostToTrimmableTypeMapInputs). -->
88+
<FilesToPackage Include="$(_MonoAndroidNETDefaultOutDir)ref\Microsoft.Android.Runtime.NativeAOT.dll" TargetPath="tools\typemap-refs" />
8389
<FilesToPackage Include="$(XamarinAndroidSourcePath)src\Xamarin.Android.Build.Tasks\Microsoft.Android.Sdk\Sdk\**" TargetPath="Sdk" />
8490
<FilesToPackage Include="$(XamarinAndroidSourcePath)src\Microsoft.Android.Sdk.ILLink\PreserveLists\**" TargetPath="PreserveLists" />
8591
<FilesToPackage Include="$(XamarinAndroidSourcePath)src\Xamarin.Android.Build.Tasks\Microsoft.Android.Sdk\PreserveLists\**" TargetPath="PreserveLists" />

src/Microsoft.Android.Runtime.NativeAOT/Microsoft.Android.Runtime.NativeAOT.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
<OutputPath>$(_MonoAndroidNETDefaultOutDir)</OutputPath>
1515
<RootNamespace>Microsoft.Android.Runtime</RootNamespace>
1616
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
17+
<!-- Produce a reference assembly so the trimmable typemap generator can scan this
18+
runtime-only host assembly (which has no other reference-assembly counterpart) for
19+
its Java Callable Wrapper types (e.g. UncaughtExceptionMarshaler) in the RID-independent
20+
outer build. See _CopyToPackDirs below and Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets. -->
21+
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
1722
</PropertyGroup>
1823

1924
<ItemGroup>
@@ -44,6 +49,18 @@
4449
DestinationFolder="$(BuildOutputDirectory)lib\packs\Microsoft.Android.Runtime.%(_RuntimePackFiles.AndroidRuntime).$(AndroidApiLevel).%(_RuntimePackFiles.AndroidRID)\$(AndroidPackVersion)\runtimes\%(_RuntimePackFiles.AndroidRID)\lib\$(DotNetTargetFramework)"
4550
SkipUnchangedFiles="true"
4651
/>
52+
<!-- Copy the reference assembly into the local Microsoft.Android.Sdk pack so the trimmable
53+
typemap generator can scan it during app builds (see
54+
Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets). It is deliberately NOT placed
55+
in the Microsoft.Android.Ref targeting pack: files under a targeting pack's ref/ folder must
56+
all be classified in its FrameworkList and would become universal framework references for
57+
every Android app. Shipping it in the SDK pack keeps it a build-time-only input for the
58+
trimmable NativeAOT path. -->
59+
<Copy
60+
SourceFiles="$(OutputPath)ref\Microsoft.Android.Runtime.NativeAOT.dll"
61+
DestinationFolder="$(MicrosoftAndroidSdkOutDir)typemap-refs"
62+
SkipUnchangedFiles="true"
63+
/>
4764
</Target>
4865

4966
</Project>

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(PrivateSdkAssemblies->'_%(Filename).TypeMap')" />
2828
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(ReferencePath->'_%(Filename).TypeMap')"
2929
Condition=" '%(ReferencePath.FrameworkAssembly)' == 'true' " />
30+
<!-- The NativeAOT runtime host (Microsoft.Android.Runtime.NativeAOT) is a framework/runtime
31+
assembly with no @(ReferencePath) counterpart in the inner build, so its per-assembly
32+
typemap DLL is not caught by the rules above. Classify it as framework so it stays an
33+
IlcReference (ILC reads its conditional TypeMap attributes) but is removed from the
34+
unmanaged-entrypoint roots, matching Mono.Android/Java.Interop. Its Java Callable Wrapper
35+
native methods register via the runtime registerNatives path. -->
36+
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="_Microsoft.Android.Runtime.NativeAOT.TypeMap" />
3037
<!-- The root assembly (_Microsoft.Android.TypeMaps) only contains TypeMapLoader.Initialize()
3138
and assembly-level attributes — no types that need vtable generation.
3239
Framework per-assembly typemap DLLs must remain IlcReference inputs so ILC can read
@@ -66,6 +73,35 @@
6673
BeforeTargets="_ResolveAssemblies"
6774
DependsOnTargets="_GenerateTrimmableTypeMap" />
6875

76+
<!--
77+
The trimmable typemap generator runs in the OUTER build over @(ReferencePath), which contains
78+
only the compile closure (app + its references + the ref-pack framework assemblies). The NativeAOT
79+
runtime host, Microsoft.Android.Runtime.NativeAOT, is a runtime-only assembly resolved solely in
80+
the per-RID inner build (as a RID-specific runtime pack asset), so it is never in @(ReferencePath)
81+
and its Java Callable Wrapper types (e.g. UncaughtExceptionMarshaler) would be missing from the
82+
typemap and JCWs -> R8 has nothing to keep -> on-device startup crash in JavaInteropRuntime.init.
83+
84+
Its managed metadata is RID-independent (byte-identical across RIDs), so a single reference
85+
assembly is shipped in this SDK pack under tools/typemap-refs (see
86+
Microsoft.Android.Runtime.NativeAOT.csproj and build-tools/create-packs/Microsoft.Android.Sdk.proj).
87+
It is intentionally not in the Microsoft.Android.Ref targeting pack, so it never becomes a universal
88+
framework reference. Feed it to the generator as an extra framework input in the RID-independent
89+
outer build; this target is the only place it enters the build.
90+
-->
91+
<Target Name="_AddNativeAotRuntimeHostToTrimmableTypeMapInputs"
92+
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(_OuterIntermediateOutputPath)' == '' "
93+
BeforeTargets="_GenerateTrimmableTypeMap">
94+
<PropertyGroup>
95+
<_NativeAotRuntimeHostRefAssembly>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\tools\typemap-refs\Microsoft.Android.Runtime.NativeAOT.dll'))</_NativeAotRuntimeHostRefAssembly>
96+
</PropertyGroup>
97+
<ItemGroup>
98+
<_AndroidTrimmableTypeMapExtraFrameworkAssembly Include="$(_NativeAotRuntimeHostRefAssembly)"
99+
Condition=" Exists('$(_NativeAotRuntimeHostRefAssembly)') " />
100+
</ItemGroup>
101+
<Warning Condition=" !Exists('$(_NativeAotRuntimeHostRefAssembly)') "
102+
Text="The NativeAOT runtime host reference assembly was not found at '$(_NativeAotRuntimeHostRefAssembly)'; runtime Java Callable Wrappers (e.g. UncaughtExceptionMarshaler) may be missing from the typemap." />
103+
</Target>
104+
69105
<Target Name="_CollectTrimmableNativeAotDgmlFiles"
70106
Condition=" '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' ">
71107
<ItemGroup>

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
<Target Name="_GenerateTrimmableTypeMap"
9999
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(DesignTimeBuild)' != 'true' and '@(ReferencePath->Count())' != '0' and '$(_OuterIntermediateOutputPath)' == '' "
100100
AfterTargets="CoreCompile"
101-
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
101+
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(_AndroidTrimmableTypeMapExtraFrameworkAssembly);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
102102
Outputs="$(_TrimmableTypeMapOutputStamp)">
103103

104104
<ItemGroup>
@@ -107,9 +107,14 @@
107107
<_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" />
108108
<_TypeMapInputAssemblies Include="@(PrivateSdkAssemblies)" />
109109
<_TypeMapInputAssemblies Include="@(FrameworkAssemblies)" />
110+
<!-- Runtime-only framework assemblies that have no counterpart in @(ReferencePath) (e.g. the
111+
NativeAOT runtime host Microsoft.Android.Runtime.NativeAOT.dll, injected by the trimmable
112+
NativeAOT targets). They carry Java Callable Wrapper types that must be in the typemap. -->
113+
<_TypeMapInputAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" />
110114
<_TypeMapFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" />
111115
<_TypeMapFrameworkAssemblies Include="@(PrivateSdkAssemblies)" />
112116
<_TypeMapFrameworkAssemblies Include="@(FrameworkAssemblies)" />
117+
<_TypeMapFrameworkAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" />
113118
<_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)"
114119
Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" />
115120
</ItemGroup>

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,38 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease
6767
}
6868
}
6969

70+
[Test]
71+
public void Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws ()
72+
{
73+
const bool isRelease = true;
74+
if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) {
75+
return;
76+
}
77+
78+
var proj = new XamarinAndroidApplicationProject {
79+
IsRelease = isRelease,
80+
LinkTool = "r8",
81+
};
82+
proj.SetRuntime (AndroidRuntime.NativeAOT);
83+
proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable");
84+
85+
using var builder = CreateApkBuilder ();
86+
Assert.IsTrue (builder.Build (proj), "Build should have succeeded.");
87+
88+
var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex"));
89+
FileAssert.Exists (dexFile);
90+
91+
// Regression test: the NativeAOT runtime host assembly (Microsoft.Android.Runtime.NativeAOT) is
92+
// resolved only in the per-RID inner build, so the RID-independent outer-build trimmable typemap
93+
// generator never scanned it. Its only Java Callable Wrapper type, UncaughtExceptionMarshaler,
94+
// therefore had no JCW and no typemap entry -> the runtime ACW is absent from classes.dex and the
95+
// app crashes at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler). A
96+
// reference assembly for the host is now shipped in the SDK pack and fed to the generator. The JCW
97+
// name is CRC-hashed (e.g. `scrc64...UncaughtExceptionMarshaler`), so match on the type name suffix.
98+
Assert.IsTrue (DexUtils.ContainsClass ("UncaughtExceptionMarshaler;", dexFile, AndroidSdkPath),
99+
$"`{dexFile}` should include the UncaughtExceptionMarshaler runtime ACW.");
100+
}
101+
70102
[Test]
71103
public void Build_WithTrimmableTypeMap_DeletesStaleGeneratedJavaSourcesAndCopies ()
72104
{

0 commit comments

Comments
 (0)