Skip to content

[NativeAOT] Take over native linker invocation from ILC targets#11148

Open
sbomer wants to merge 3 commits intomainfrom
dev/sbomer/nativeaot-linker-takeover
Open

[NativeAOT] Take over native linker invocation from ILC targets#11148
sbomer wants to merge 3 commits intomainfrom
dev/sbomer/nativeaot-linker-takeover

Conversation

@sbomer
Copy link
Copy Markdown
Member

@sbomer sbomer commented Apr 17, 2026

Set NativeLib=static so ILC produces a .a archive via ar instead of invoking the linker directly. Add _AndroidLinkNativeAotSharedLibrary target that runs after LinkNative and links the ILC .o output into a .so using the NDK clang wrapper. This gives Android full control over the native linker invocation, following the same approach used by macios.

Reproduce the flags that LinkNative and SetupOSSpecificProps would have provided for NativeLib=Shared:

  • -shared, -Wl,-e,0x0, -Wl,-z,max-page-size=16384 (from LinkerArg)
  • --version-script, --export-dynamic, --discard-all, --gc-sections (from CustomLinkerArg inside LinkNative)
  • -fuse-ld=lld (from LinkerArg via LinkerFlavor)
  • sections.ld linker script to retain the __modules section

Set IlcExportUnmanagedEntrypoints=true so ILC exports [UnmanagedCallersOnly] methods as native symbols, required for JNI entry points.

Clear LinkerFlavor inside _AndroidBeforeIlcCompile to work around an ILC targets bug where _LinkerVersion detection is skipped for NativeLib=Static but the numeric comparison in LinkNative still evaluates. Context: dotnet/runtime#126978

The resulting linker command line is identical to the original.

Contributes to #10697

Set NativeLib=static so ILC produces a .a archive via ar instead of invoking
the linker directly. Add _AndroidLinkNativeAotSharedLibrary target that runs
after LinkNative and links the ILC .o output into a .so using the NDK clang
wrapper. This gives Android full control over the native linker invocation,
following the same approach used by macios.

Reproduce the flags that LinkNative and SetupOSSpecificProps would have
provided for NativeLib=Shared:
- -shared, -Wl,-e,0x0, -Wl,-z,max-page-size=16384 (from LinkerArg)
- --version-script, --export-dynamic, --discard-all, --gc-sections (from
  CustomLinkerArg inside LinkNative)
- -fuse-ld=lld (from LinkerArg via LinkerFlavor)
- sections.ld linker script to retain the __modules section

Set IlcExportUnmanagedEntrypoints=true so ILC exports [UnmanagedCallersOnly]
methods as native symbols, required for JNI entry points.

Clear LinkerFlavor inside _AndroidBeforeIlcCompile to work around an ILC
targets bug where _LinkerVersion detection is skipped for NativeLib=Static
but the numeric comparison in LinkNative still evaluates.
Context: dotnet/runtime#126978

The resulting linker command line is identical to the original.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sbomer sbomer requested a review from jonathanpeppers as a code owner April 17, 2026 17:30
Copilot AI review requested due to automatic review settings April 17, 2026 17:30
@sbomer sbomer requested a review from simonrozsival as a code owner April 17, 2026 17:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the NativeAOT MSBuild flow for .NET for Android so Android (NDK clang wrapper) performs the final shared-library link step instead of ILC, aligning linker invocation control with other platforms (e.g., macios).

Changes:

  • Set NativeLib=static so ILC produces a static archive rather than directly linking a .so.
  • Add _AndroidLinkNativeAotSharedLibrary to link ILC output into a .so using the NDK clang wrapper and restore required linker flags/scripts.
  • Ensure unmanaged entrypoints are exported and tweak linker-related properties/soname/publish naming to match Android expectations.

Add Inputs/Outputs to _AndroidLinkNativeAotSharedLibrary so incremental
builds can skip relinking when inputs haven't changed. Add FileWrites
for the .so and sections.ld so Clean can account for generated files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sbomer sbomer self-assigned this Apr 17, 2026
Comment on lines +278 to +281
<Target Name="_AndroidLinkNativeAotSharedLibrary"
AfterTargets="LinkNative"
Inputs="$(NativeObject);@(NativeLibrary)"
Outputs="$(NativeOutputPath)$(NativeBinaryPrefix)$(TargetName).so">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do managed assemblies also need to be Inputs? Someone could add their own [UnmanagedCallersOnly] and it wouldn't get picked up in an incremental build.

Honestly, this is NativeAOT (Release-mode only), maybe we could just let this always run if LinkNative ran?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if someone adds [UCO] it would cause ILC to re-run, produce a new $(NativeObject) and that would make this target run.

These are the same inputs/outputs as the LinkNative target so it should already be running if LinkNative ran. If we remove the Inputs/Outputs, it would always run even if LinkNative was skipped.

@jonathanpeppers
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 17, 2026

Android PR Reviewer completed successfully!

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review Summary

Verdict: ✅ LGTM

Found 0 errors, 0 warnings, 2 suggestions.

Analysis

The PR correctly transitions NativeAOT from delegating shared library linking to ILC's LinkNative target to having Android handle it directly via _AndroidLinkNativeAotSharedLibrary. I verified the approach against the ILC targets in dotnet/runtime:

  • @(NativeLibrary) items are correctly consumed — ILC's Microsoft.NETCore.Native.Unix.targets merges them into @(LinkerArg) (line 243 in the runtime), so @(LinkerArg) is the single source for all linker inputs. ✅
  • NativeLib=static + IlcExportUnmanagedEntrypoints=true correctly makes ILC produce a .a via ar while still exporting JNI entry points. ✅
  • LinkerFlavor clearing is a sound workaround for dotnet/runtime#126978 — with NativeLib=static, ILC skips _LinkerVersion detection but still evaluates a numeric comparison. ✅
  • Hardcoded .so replacing $(NativeBinaryExt) is correct — with NativeLib=static, $(NativeBinaryExt) would be .a, which is wrong for soname and file matching. ✅
  • Incremental build supportInputs/Outputs and FileWrites are properly set. The FileWrites items in the <ItemGroup> inside the target will evaluate even when the target is skipped. ✅
  • Linker flags reproduce what ILC's LinkNative + SetupOSSpecificProps would provide for NativeLib=Shared (verified against the runtime source). ✅

CI Status

  • 🟢 Linux: passed
  • 🔴 Windows: failed (unable to access Azure DevOps logs to confirm if related to this PR or pre-existing)
  • 🟡 macOS: in progress
  • ⏳ Overall: queued

Suggestions

  • 💡 Add a comment explaining that @(NativeLibrary) is consumed indirectly via @(LinkerArg) (Microsoft.Android.Sdk.NativeAOT.targets:299)
  • 💡 Document why sections.ld and --gc-sections are unconditional (NDK always uses LLD) (Microsoft.Android.Sdk.NativeAOT.targets:298)

👍 Well-structured change with excellent inline documentation. The approach of taking over the linker invocation (matching what macios does) gives Android full control over the native link step, which is the right architectural direction.


Review generated by android-reviewer from review guidelines.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dev.azure.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dev.azure.com"

See Network Configuration for more information.

Generated by Android PR Reviewer for issue #11148 · ● 9.6M

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants