[NativeAOT] Use NativeLinker and invoke lld directly for linking#11256
[NativeAOT] Use NativeLinker and invoke lld directly for linking#11256
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>
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>
On Windows, ILC's LinkNative writes linker args to a response file instead of passing them inline — cmd.exe has quoting issues with spaces in paths. Match that behavior in _AndroidLinkNativeAotSharedLibrary. Fixes NativeAOT build failures for projects with spaces or special characters in their names (e.g. CheckProjectWithSpaceInNameWorks tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ILC's LinkNative target defaults CppLibCreator to the host 'ar', which does not understand ELF objects when cross-compiling for Android. On macOS this caused Xcode's ranlib to emit spurious 'empty table of contents' warnings for every ABI. Set CppLibCreator to llvm-ar (from the NDK toolchain, already on PATH) so the archiver can correctly process ELF .o files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit 2ff10fb.
ILC's LinkNative target strips symbols via llvm-objcopy after linking, but those steps are skipped when NativeLib=Static. Add the same three objcopy invocations to _AndroidLinkNativeAotSharedLibrary: 1. Extract debug info to .dbg file 2. Strip debug symbols from the .so 3. Add gnu-debuglink back to the .so Without stripping, the .so was ~8MB larger than the original, causing BuildReleaseArm64 apkdiff regression tests to fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add opt-in properties to NativeLinker.cs for NativeAOT-specific flags: ExportDynamic, UseEhFrameHdr, DiscardAll, AsNeeded, HashStyleBoth, LittleEndian, VersionScript, LinkerScript, EntryPoint, CompressDebugSections, AdditionalSearchPaths. Move --export-dynamic from standardArgs to an opt-in ExportDynamic property (default true for back-compat with existing consumers). Add LinkNativeAotSharedLibrary task that uses NativeLinker to link the ILC .o output into a .so using ld.lld directly, replacing the clang wrapper invocation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the clang Exec invocation with the LinkNativeAotSharedLibrary task that calls ld.lld directly from the NDK. The task uses NativeLinker.cs which handles response files, ABI-specific flags, and debug symbol stripping. Compute NDK paths for CRT objects (crtbegin_so.o/crtend_so.o), compiler-rt builtins, and libunwind. Pass system libraries (dl, z, log, m, c) and library search paths explicitly. Fix NativeLinker.cs quoting for soname with spaces, and guard against empty runtime pack library directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR shifts NativeAOT shared-library linking to use the existing NativeLinker infrastructure (response files, ABI flags, symbol stripping) instead of invoking the NDK clang wrapper, and wires this into the NativeAOT MSBuild targets.
Changes:
- Extend
NativeLinkerwith NativeAOT-focused linker options (export-dynamic, version scripts, linker scripts, debug section compression, extra-Lpaths, etc.). - Add a new MSBuild task (
LinkNativeAotSharedLibrary) that drivesNativeLinkerto produce a.sofrom ILC output + runtime/static archives. - Update
Microsoft.Android.Sdk.NativeAOT.targetsto setNativeLib=static, enable unmanaged entrypoint exports, and run the new link step afterLinkNative.
Show a summary per file
| File | Description |
|---|---|
src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs |
Adds configurable linker options needed for NativeAOT and fixes argument quoting/guards. |
src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotSharedLibrary.cs |
New task orchestrating NativeAOT .so linking via NativeLinker. |
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets |
MSBuild integration: produce .a from ILC then link final .so via the new task, plus NDK CRT/rt/unwind inputs. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeAotSharedLibrary.cs:140
Directory.CreateDirectory (Path.GetDirectoryName (LinkerScript)!)can throw ifLinkerScriptis just a filename (no directory component), becausePath.GetDirectoryName()would returnnull. Avoid the null-forgiving operator here and handle thenullcase explicitly (or useIntermediateOutputPathas the directory when none is provided).
if (!LinkerScriptContent.IsNullOrEmpty () && !LinkerScript.IsNullOrEmpty ()) {
Directory.CreateDirectory (Path.GetDirectoryName (LinkerScript)!);
File.WriteAllText (LinkerScript, LinkerScriptContent);
}
- Files reviewed: 3/3 changed files
- Comments generated: 4
# Conflicts: # src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
don't strip and save debug symbols separately. Matches the pattern used by LinkApplicationSharedLibraries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use TaskItem copy constructor instead of creating from ItemSpec alone, so metadata like NativeLinkWholeArchive and NativeDontExportSymbols is preserved. NativeLinker checks these for --whole-archive and --exclude-libs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
⚠️ Needs Changes
Summary: This PR replaces the raw Exec clang invocation in _AndroidLinkNativeAotSharedLibrary with a proper LinkNativeAotSharedLibrary MSBuild task that calls ld.lld directly via the shared NativeLinker infrastructure. The approach is solid — it consolidates NativeAOT linking with the existing CoreCLR/Mono linking, provides better structure, and brings NativeAOT in line with how other runtimes are linked.
Issues
| Severity | Count |
|---|---|
| 3 | |
| 💡 Suggestion | 1 |
⚠️ null!usage inLinkNativeAotSharedLibrary.cs(lines 33, 39, 138) — three uses of the banned null-forgiving operator⚠️ MissingFileWritesfor.dbg.so— NativeLinker creates debug symbol files in release builds that aren't tracked, risking IncrementalClean deletion- 💡 Misleading "NativeAOT-specific" comment in NativeLinker.cs —
ExportDynamicis actually the inherited default for all callers
Positive callouts
- 🎯 Clean refactoring of the linker invocation from raw
Execto a typed MSBuild task with structured parameters - 🎯 Good defensive fix: soname quoting (
QuoteFileNameArgument) and null guard on empty runtime pack directory - 🎯 Backward-compatible changes to
NativeLinker.cs— existing callers (LinkApplicationSharedLibraries,LinkNativeRuntime) are unaffected by the removal of--export-dynamicand-z max-page-sizefromstandardArgs(both are now handled correctly via properties and constructor logic) - 🎯 CI is green (both public and internal Xamarin.Android-PR checks pass)
Generated by Android PR Reviewer for issue #11256 · ● 4.6M
Add .dbg FileWrites entry so Clean can delete the debug symbol file produced by NativeLinker.ExtractDebugSymbols in Release builds. Fix comment on NativeLinker properties from 'NativeAOT-specific' to 'Additional linker options' since ExportDynamic replaced the previous standardArgs entry and applies to all callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use explicit null check for Path.GetDirectoryName result instead of the ! operator, following repo nullable conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the clang Exec invocation with the LinkNativeAotSharedLibrary task
that calls ld.lld directly from the NDK. The task uses NativeLinker.cs which
handles response files, ABI-specific flags, and debug symbol stripping.
Compute NDK paths for CRT objects (crtbegin_so.o/crtend_so.o), compiler-rt
builtins, and libunwind. Pass system libraries (dl, z, log, m, c) and
library search paths explicitly.
Fix NativeLinker.cs quoting for soname with spaces, and guard against empty
runtime pack library directory.
Builds on #11148.
Contributes to #10697