Skip to content

Commit c4dbc85

Browse files
[TrimmableTypeMap] Propagate deferred registerNatives to base classes (#11105)
## Summary Fixes a startup crash (`UnsatisfiedLinkError: No implementation found for void mono.android.Runtime.registerNatives`) when running device tests with the trimmable typemap enabled. ## Background Deferred `registerNatives` support was introduced across several PRs: - **#10957** — Added `CannotRegisterInStaticConstructor` flag and set it in `JavaPeerScanner` for types with `[Application]` or `[Instrumentation]` attributes. The JCW generator skips the `static { registerNatives(...); }` block for these types and emits a lazy `__md_registerNatives()` helper instead. - **#11037** — Extended deferred registration to manifest-rooted types: `RootManifestReferencedTypes` sets `CannotRegisterInStaticConstructor` on types matched by `<application>` or `<instrumentation>` manifest entries. - **#11096** / **#11097** — Further scanner and JCW edge-case fixes, including deferred registration propagation for generated managed base types. **None of these PRs propagated the flag to base classes in the Java type hierarchy.** When `NUnitInstrumentation` (listed in the manifest) correctly uses deferred registration, its base class `TestInstrumentation_1` still emits `static { registerNatives(...); }`. Since Java loads the base class `<clinit>` before the managed runtime registers the JNI native method, this crashes with `UnsatisfiedLinkError`. ## Changes Add `PropagateDeferredRegistrationToBaseClasses()` in `TrimmableTypeMapGenerator`, called after `RootManifestReferencedTypes`. It walks each flagged peer's `BaseJavaName` chain with a simple linear scan and sets `CannotRegisterInStaticConstructor = true` on all base peers that have JCW stubs (i.e., `!DoNotGenerateAcw`). Framework MCW types are skipped. In practice only 1–2 types need propagation (one Application, maybe one Instrumentation), each with a short base-class chain. A linear scan per ancestor is simpler and cheaper than building a `Dictionary<JavaName, List<Peer>>` lookup over all peers up front. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8df9772 commit c4dbc85

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public TrimmableTypeMapResult Execute (
3939
}
4040

4141
RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig));
42+
PropagateDeferredRegistrationToBaseClasses (allPeers);
4243

4344
var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion);
4445
var jcwPeers = allPeers.Where (p =>
@@ -207,6 +208,42 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
207208
}
208209
}
209210

211+
/// <summary>
212+
/// Propagates <see cref="JavaPeerInfo.CannotRegisterInStaticConstructor"/> up the base class chain.
213+
/// When a type like NUnitInstrumentation has deferred registration, its base class
214+
/// TestInstrumentation_1 must also defer — otherwise the base class <c>&lt;clinit&gt;</c> will call
215+
/// <c>registerNatives</c> before the managed runtime is ready.
216+
/// </summary>
217+
internal static void PropagateDeferredRegistrationToBaseClasses (List<JavaPeerInfo> allPeers)
218+
{
219+
// In practice only 1–2 types need propagation (one Application, maybe one
220+
// Instrumentation), each with a short base-class chain. A linear scan per
221+
// ancestor is simpler and cheaper than building a Dictionary<JavaName, List<Peer>>
222+
// lookup over all peers up front.
223+
foreach (var peer in allPeers) {
224+
if (peer.CannotRegisterInStaticConstructor) {
225+
PropagateToAncestors (peer.BaseJavaName, allPeers);
226+
}
227+
}
228+
229+
static void PropagateToAncestors (string? baseJniName, List<JavaPeerInfo> allPeers)
230+
{
231+
while (baseJniName is not null) {
232+
string? nextBase = null;
233+
foreach (var basePeer in allPeers) {
234+
if (!string.Equals (basePeer.JavaName, baseJniName, StringComparison.Ordinal) || basePeer.DoNotGenerateAcw) {
235+
continue;
236+
}
237+
238+
basePeer.CannotRegisterInStaticConstructor = true;
239+
nextBase = basePeer.BaseJavaName;
240+
}
241+
242+
baseJniName = nextBase;
243+
}
244+
}
245+
}
246+
210247
static void AddPeerByDotName (Dictionary<string, List<JavaPeerInfo>> peersByDotName, string dotName, JavaPeerInfo peer)
211248
{
212249
if (!peersByDotName.TryGetValue (dotName, out var list)) {

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,52 @@ public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes
226226
Assert.True (peers [1].CannotRegisterInStaticConstructor, "Instrumentation type should defer Runtime.registerNatives().");
227227
}
228228

229+
[Fact]
230+
public void PropagateDeferredRegistrationToBaseClasses_PropagatesToBaseClassesOfManifestReferencedTypes ()
231+
{
232+
var basePeer = new JavaPeerInfo {
233+
JavaName = "crc64aaa/TestInstrumentation_1", CompatJniName = "crc64aaa/TestInstrumentation_1",
234+
ManagedTypeName = "Tests.TestInstrumentation`1", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "TestInstrumentation`1",
235+
AssemblyName = "Tests", IsUnconditional = false,
236+
BaseJavaName = "android/app/Instrumentation",
237+
};
238+
var midPeer = new JavaPeerInfo {
239+
JavaName = "crc64bbb/NUnitTestInstrumentation", CompatJniName = "crc64bbb/NUnitTestInstrumentation",
240+
ManagedTypeName = "Tests.NUnitTestInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitTestInstrumentation",
241+
AssemblyName = "Tests", IsUnconditional = false,
242+
BaseJavaName = "crc64aaa/TestInstrumentation_1",
243+
};
244+
var leafPeer = new JavaPeerInfo {
245+
JavaName = "crc64ccc/NUnitInstrumentation", CompatJniName = "crc64ccc/NUnitInstrumentation",
246+
ManagedTypeName = "Tests.NUnitInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitInstrumentation",
247+
AssemblyName = "Tests", IsUnconditional = false,
248+
BaseJavaName = "crc64bbb/NUnitTestInstrumentation",
249+
};
250+
var peers = new List<JavaPeerInfo> { basePeer, midPeer, leafPeer };
251+
252+
var doc = System.Xml.Linq.XDocument.Parse ("""
253+
<?xml version="1.0" encoding="utf-8"?>
254+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
255+
<instrumentation android:name="crc64ccc.NUnitInstrumentation" />
256+
</manifest>
257+
""");
258+
259+
var generator = CreateGenerator ();
260+
generator.RootManifestReferencedTypes (peers, doc);
261+
262+
// RootManifestReferencedTypes sets the flag only on the directly matched leaf
263+
Assert.True (leafPeer.CannotRegisterInStaticConstructor, "Leaf instrumentation should have deferred registration after manifest rooting.");
264+
Assert.False (midPeer.CannotRegisterInStaticConstructor, "Mid peer should NOT have deferred registration before propagation.");
265+
Assert.False (basePeer.CannotRegisterInStaticConstructor, "Base peer should NOT have deferred registration before propagation.");
266+
267+
// PropagateDeferredRegistrationToBaseClasses walks the BaseJavaName chain
268+
TrimmableTypeMapGenerator.PropagateDeferredRegistrationToBaseClasses (peers);
269+
270+
Assert.True (leafPeer.CannotRegisterInStaticConstructor, "Leaf instrumentation should still have deferred registration.");
271+
Assert.True (midPeer.CannotRegisterInStaticConstructor, "Mid peer should have deferred registration after propagation.");
272+
Assert.True (basePeer.CannotRegisterInStaticConstructor, "Base peer should have deferred registration after propagation.");
273+
}
274+
229275
[Fact]
230276
public void RootManifestReferencedTypes_WarnsForUnresolvedTypes ()
231277
{

0 commit comments

Comments
 (0)