Skip to content

[Trimmable Type Map] Unify native-method registration and remove ApplicationRegistration / __md_registerNatives #11147

@simonrozsival

Description

@simonrozsival

Background

Today the trimmable typemap JCW generator (JcwJavaSourceGenerator.WriteStaticInitializer / WriteMethods) uses two parallel mechanisms to bind native callbacks:

  1. Normal peer typesstatic { mono.android.Runtime.registerNatives (...); } in the class initializer. This runs on first class access, so native bindings are in place before any method dispatch.
  2. 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:

  1. Normal types: keep static { mono.android.Runtime.registerNatives (...); } — it is free, correct, and triggered by the JVM's class-loading rules.
  2. 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.
  3. 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).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    copilot`copilot-cli` or other AIs were used to author thisneeds-triageIssues that need to be assigned.trimmable-type-map

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions