Background
Today the trimmable typemap JCW generator (JcwJavaSourceGenerator.WriteStaticInitializer / WriteMethods) uses two parallel mechanisms to bind native callbacks:
- Normal peer types —
static { mono.android.Runtime.registerNatives (...); } in the class initializer. This runs on first class access, so native bindings are in place before any method dispatch.
Application / Instrumentation subtypes (JavaPeerInfo.CannotRegisterInStaticConstructor) — the static initializer is omitted, an ApplicationRegistration.registerApplications () aggregator is emitted by GenerateTrimmableTypeMap, and every generated native-method wrapper is prefixed with a lazy private static synchronized __md_registerNatives () guard. The lazy helper exists to cover Application.attachBaseContext, which can fire before MonoRuntimeProvider.attachInfo has run registerApplications ().
This layered scheme is complex:
- Two registration paths to maintain.
ApplicationRegistration.java is generated per app and must be kept in sync with manifest android:name scanning.
JavaPeerInfo.CannotRegisterInStaticConstructor has to be propagated onto derived user Application/Instrumentation types (a source of past bugs).
- Every native-method wrapper pays a
synchronized + field-load even after the runtime is ready.
- Static
[Export] methods are not fully supported in the trimmable path. MarshalMethodInfo has no IsStatic field, and WriteMethods never emits the static keyword. If the generator ever starts emitting static marshal methods on CannotRegisterInStaticConstructor types, each of them would independently need __md_registerNatives() prepended — yet another place the current design has to be carefully preserved.
Proposal
Collapse registration onto a single path, and move the "too early" fix out of the generator and into the runtime:
- Normal types: keep
static { mono.android.Runtime.registerNatives (...); } — it is free, correct, and triggered by the JVM's class-loading rules.
Application / Instrumentation types: register their natives from the runtime during JNIEnv.Initialize (or the equivalent trimmable entry point), early enough that the first Application / Instrumentation call can already find its native bindings. We would discover the set of classes to register from the same scan that populates ApplicationRegistrationTypes today, but we would call registerNatives from managed code at init rather than from a generated Java aggregator.
- Delete:
ApplicationRegistration.java (both the resource template and the generator that synthesizes it per app).
- The
__md_registerNatives () helper and the per-wrapper prelude.
- The
JavaPeerInfo.CannotRegisterInStaticConstructor propagation logic in the scanner / generator (we no longer need two code paths).
- Static marshal methods: once the registration story is uniform, adding
IsStatic to MarshalMethodInfo and emitting the static keyword in WriteMethods becomes a pure generator change — no interaction with registration ordering.
Questions to resolve
- Is
JNIEnv.Initialize always reached before the first user-overridden Application.attachBaseContext call? For legacy Xamarin.Android this was the motivating problem; for the trimmable path (via MonoRuntimeProvider.attachInfo -> MonoPackageManager.LoadApplication) the ordering may already be tighter. Audit the provider chain and confirm.
- Can we register the Application/Instrumentation natives from JNI without holding managed-side locks that might deadlock if Android is currently stuck on a native call?
- Measurement: the per-call
__md_registerNatives () check is tiny but non-zero. After removal, is startup / steady-state measurably affected? (Expected: net-neutral or faster.)
Scope
- Generator: delete
ApplicationRegistration emission, simplify JcwJavaSourceGenerator.WriteStaticInitializer to always emit the static { registerNatives (...); } initializer, delete the __md_registerNatives helper and prelude.
- Scanner:
JavaPeerInfo.CannotRegisterInStaticConstructor and its propagation can be deleted; add MarshalMethodInfo.IsStatic to unblock static [Export].
- Runtime: extend
JNIEnv.Initialize (or the trimmable-mode equivalent) to register natives for the manifest-referenced Application / Instrumentation types before returning.
- Tests: regression tests for the
attachBaseContext-before-attachInfo ordering; a static [Export] marshal-method test once the generator change lands.
Tracked under the Trimmable Type Map epic #10788.
Background
Today the trimmable typemap JCW generator (
JcwJavaSourceGenerator.WriteStaticInitializer/WriteMethods) uses two parallel mechanisms to bind native callbacks:static { mono.android.Runtime.registerNatives (...); }in the class initializer. This runs on first class access, so native bindings are in place before any method dispatch.Application/Instrumentationsubtypes (JavaPeerInfo.CannotRegisterInStaticConstructor) — the static initializer is omitted, anApplicationRegistration.registerApplications ()aggregator is emitted byGenerateTrimmableTypeMap, and every generated native-method wrapper is prefixed with a lazyprivate static synchronized __md_registerNatives ()guard. The lazy helper exists to coverApplication.attachBaseContext, which can fire beforeMonoRuntimeProvider.attachInfohas runregisterApplications ().This layered scheme is complex:
ApplicationRegistration.javais generated per app and must be kept in sync with manifestandroid:namescanning.JavaPeerInfo.CannotRegisterInStaticConstructorhas to be propagated onto derived userApplication/Instrumentationtypes (a source of past bugs).synchronized+ field-load even after the runtime is ready.[Export]methods are not fully supported in the trimmable path.MarshalMethodInfohas noIsStaticfield, andWriteMethodsnever emits thestatickeyword. If the generator ever starts emitting static marshal methods onCannotRegisterInStaticConstructortypes, each of them would independently need__md_registerNatives()prepended — yet another place the current design has to be carefully preserved.Proposal
Collapse registration onto a single path, and move the "too early" fix out of the generator and into the runtime:
static { mono.android.Runtime.registerNatives (...); }— it is free, correct, and triggered by the JVM's class-loading rules.Application/Instrumentationtypes: register their natives from the runtime duringJNIEnv.Initialize(or the equivalent trimmable entry point), early enough that the firstApplication/Instrumentationcall can already find its native bindings. We would discover the set of classes to register from the same scan that populatesApplicationRegistrationTypestoday, but we would callregisterNativesfrom managed code at init rather than from a generated Java aggregator.ApplicationRegistration.java(both the resource template and the generator that synthesizes it per app).__md_registerNatives ()helper and the per-wrapper prelude.JavaPeerInfo.CannotRegisterInStaticConstructorpropagation logic in the scanner / generator (we no longer need two code paths).IsStatictoMarshalMethodInfoand emitting thestatickeyword inWriteMethodsbecomes a pure generator change — no interaction with registration ordering.Questions to resolve
JNIEnv.Initializealways reached before the first user-overriddenApplication.attachBaseContextcall? For legacy Xamarin.Android this was the motivating problem; for the trimmable path (viaMonoRuntimeProvider.attachInfo->MonoPackageManager.LoadApplication) the ordering may already be tighter. Audit the provider chain and confirm.__md_registerNatives ()check is tiny but non-zero. After removal, is startup / steady-state measurably affected? (Expected: net-neutral or faster.)Scope
ApplicationRegistrationemission, simplifyJcwJavaSourceGenerator.WriteStaticInitializerto always emit thestatic { registerNatives (...); }initializer, delete the__md_registerNativeshelper and prelude.JavaPeerInfo.CannotRegisterInStaticConstructorand its propagation can be deleted; addMarshalMethodInfo.IsStaticto unblock static[Export].JNIEnv.Initialize(or the trimmable-mode equivalent) to register natives for the manifest-referencedApplication/Instrumentationtypes before returning.attachBaseContext-before-attachInfoordering; a static[Export]marshal-method test once the generator change lands.Tracked under the Trimmable Type Map epic #10788.