[NativeAOT] Take over native linker invocation from ILC targets#11148
[NativeAOT] Take over native linker invocation from ILC targets#11148
Conversation
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>
There was a problem hiding this comment.
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=staticso ILC produces a static archive rather than directly linking a.so. - Add
_AndroidLinkNativeAotSharedLibraryto link ILC output into a.sousing 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>
| <Target Name="_AndroidLinkNativeAotSharedLibrary" | ||
| AfterTargets="LinkNative" | ||
| Inputs="$(NativeObject);@(NativeLibrary)" | ||
| Outputs="$(NativeOutputPath)$(NativeBinaryPrefix)$(TargetName).so"> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 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'sMicrosoft.NETCore.Native.Unix.targetsmerges them into@(LinkerArg)(line 243 in the runtime), so@(LinkerArg)is the single source for all linker inputs. ✅NativeLib=static+IlcExportUnmanagedEntrypoints=truecorrectly makes ILC produce a.aviaarwhile still exporting JNI entry points. ✅LinkerFlavorclearing is a sound workaround for dotnet/runtime#126978 — with NativeLib=static, ILC skips_LinkerVersiondetection but still evaluates a numeric comparison. ✅- Hardcoded
.soreplacing$(NativeBinaryExt)is correct — with NativeLib=static,$(NativeBinaryExt)would be.a, which is wrong for soname and file matching. ✅ - Incremental build support —
Inputs/OutputsandFileWritesare properly set. TheFileWritesitems in the<ItemGroup>inside the target will evaluate even when the target is skipped. ✅ - Linker flags reproduce what ILC's
LinkNative+SetupOSSpecificPropswould provide forNativeLib=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.ldand--gc-sectionsare 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
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:
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