Skip to content

Commit 36b07f1

Browse files
[TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors
When switching MonoAndroidTypeMapFlavor between legacy and trimmable without a clean build, stale legacy JCWs with Runtime.register(typeName, klass, methods) calls persisted in the android/src/ intermediate directory alongside trimmable JCWs that only use Runtime.registerNatives(). Both got compiled into the APK, causing the legacy registration path to execute at runtime — a path incompatible with trimming. Root cause: the trimmable JCW generator wrote to typemap/java/ and then copied to android/src/, but never cleaned android/src/ first. Legacy JCWs with different package names survived the copy overlay. Three fixes: 1. Write trimmable JCWs directly to _AndroidIntermediateJavaSourceDirectory (android/src/) instead of a separate typemap/java/ directory, eliminating the copy step entirely. This is the same directory that _FindJavaStubFiles globs from, so no files can be missed or stale. 2. Add MonoAndroidTypeMapFlavor to the build properties cache so switching between legacy and trimmable triggers _CleanIntermediateIfNeeded, removing all stale artifacts from the previous flavor. 3. Always wire registerJniNativesFn so that if a stale JCW somehow calls Runtime.register(), TrimmableTypeMapTypeManager.RegisterNativeMembers throws UnreachableException with a clear diagnostic instead of the C++ side silently dropping the call. Also adds a unit test verifying trimmable JCWs never emit Runtime.register(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c2bdc5b commit 36b07f1

5 files changed

Lines changed: 16 additions & 16 deletions

File tree

src/Mono.Android/Android.Runtime/JNIEnvInit.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
178178

179179
args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, IntPtr, void>)&PropagateUncaughtException;
180180

181-
if (!RuntimeFeature.TrimmableTypeMap) {
182-
args->registerJniNativesFn = (IntPtr)(delegate* unmanaged<IntPtr, int, IntPtr, IntPtr, int, void>)&RegisterJniNatives;
183-
}
181+
args->registerJniNativesFn = (IntPtr)(delegate* unmanaged<IntPtr, int, IntPtr, IntPtr, int, void>)&RegisterJniNatives;
184182
RunStartupHooksIfNeeded ();
185183
SetSynchronizationContext ();
186184
}

src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public override void RegisterNativeMembers (
6767
ReadOnlySpan<char> methods)
6868
{
6969
throw new UnreachableException (
70-
$"RegisterNativeMembers should not be called in the trimmable typemap path. " +
71-
$"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks.");
70+
$"Trimmable JCWs should only call Runtime.registerNatives(klass), never " +
71+
$"Runtime.register(typeName, klass, methods). Type '{type.FullName}' triggered " +
72+
$"a Runtime.register() call, which indicates a JCW generation bug.");
7273
}
7374
}

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
<_TypeMapBaseOutputDir>$(IntermediateOutputPath)</_TypeMapBaseOutputDir>
1515
<_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/'))</_TypeMapBaseOutputDir>
1616
<_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/</_TypeMapOutputDirectory>
17-
<_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java</_TypeMapJavaOutputDirectory>
1817
</PropertyGroup>
1918

2019
<ItemGroup>
@@ -55,7 +54,7 @@
5554
<GenerateTrimmableTypeMap
5655
ResolvedAssemblies="@(_TypeMapInputAssemblies)"
5756
OutputDirectory="$(_TypeMapOutputDirectory)"
58-
JavaSourceOutputDirectory="$(_TypeMapJavaOutputDirectory)"
57+
JavaSourceOutputDirectory="$(_AndroidIntermediateJavaSourceDirectory)"
5958
TargetFrameworkVersion="$(TargetFrameworkVersion)"
6059
ManifestTemplate="$(_AndroidManifestAbs)"
6160
MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml"
@@ -99,15 +98,6 @@
9998
Inputs="@(_GenerateJavaStubsInputs)"
10099
Outputs="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp">
101100

102-
<ItemGroup>
103-
<_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" />
104-
</ItemGroup>
105-
<Copy SourceFiles="@(_TypeMapJavaFiles)" DestinationFolder="$(IntermediateOutputPath)android/src/%(RecursiveDir)" />
106-
107-
<ItemGroup>
108-
<FileWrites Include="@(_TypeMapJavaFiles->'$(IntermediateOutputPath)android/src/%(RecursiveDir)%(Filename)%(Extension)')" />
109-
</ItemGroup>
110-
111101
<!-- Set properties for ABI/RID used by CoreCLR _AddTrimmableTypeMapAssembliesToStore fallback -->
112102
<PropertyGroup>
113103
<_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0])</_TypeMapFirstAbi>

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ because xbuild doesn't support framework reference assemblies.
981981
<_PropertyCacheItems Include="AndroidManifestPlaceholders=$(AndroidManifestPlaceholders)" />
982982
<_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" />
983983
<_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" />
984+
<_PropertyCacheItems Include="MonoAndroidTypeMapFlavor=$(MonoAndroidTypeMapFlavor)" />
984985
</ItemGroup>
985986
<WriteLinesToFile
986987
File="$(_AndroidBuildPropertiesCache)"

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock ()
134134
AssertContainsLine ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java);
135135
}
136136

137+
[Fact]
138+
public void Generate_AcwType_NeverCallsRuntimeRegister ()
139+
{
140+
var java = GenerateFixture ("my/app/MainActivity");
141+
// Trimmable JCWs must only call Runtime.registerNatives(klass).
142+
// Runtime.register(typeName, klass, methods) is the legacy path that
143+
// relies on reflection-based callback registration — incompatible with trimming.
144+
Assert.DoesNotContain ("Runtime.register (\"", java);
145+
}
146+
137147
[Fact]
138148
public void Generate_ApplicationType_SkipsRegisterNatives ()
139149
{

0 commit comments

Comments
 (0)