[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
| RuntimePackLibraryDirectories | ||
| ) { | ||
| StripDebugSymbols = !DebugBuild, | ||
| SaveDebugSymbols = true, |
There was a problem hiding this comment.
SaveDebugSymbols is set to true unconditionally. With StripDebugSymbols = !DebugBuild, this causes Debug builds to still run NativeLinker.ExtractDebugSymbols() which strips debug info from the output .so. Align this with the existing linker usage by setting SaveDebugSymbols consistently with StripDebugSymbols (e.g., SaveDebugSymbols = !DebugBuild).
| SaveDebugSymbols = true, | |
| SaveDebugSymbols = !DebugBuild, |
| var nativeObj = new TaskItem (NativeObject.ItemSpec); | ||
| nativeObj.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (nativeObj); | ||
|
|
||
| foreach (var lib in NativeLibraries) { | ||
| var item = new TaskItem (lib.ItemSpec); | ||
| item.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (item); | ||
| } | ||
|
|
||
| if (SystemLibraries != null) { | ||
| foreach (var lib in SystemLibraries) { | ||
| linkItems.Add (NativeLinker.MakeLibraryItem (lib.ItemSpec, abi)); | ||
| } | ||
| } | ||
|
|
||
| if (AdditionalObjectFiles != null) { | ||
| foreach (var obj in AdditionalObjectFiles) { | ||
| var item = new TaskItem (obj.ItemSpec); | ||
| item.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (item); | ||
| } | ||
| } | ||
|
|
||
| if (CompilerRuntimeLibraries != null) { | ||
| foreach (var lib in CompilerRuntimeLibraries) { | ||
| var item = new TaskItem (lib.ItemSpec); | ||
| item.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (item); | ||
| } | ||
| } |
There was a problem hiding this comment.
The task rebuilds new TaskItem instances for NativeObject/NativeLibraries/AdditionalObjectFiles/CompilerRuntimeLibraries using only ItemSpec + Abi, which drops important metadata such as LinkWholeArchive / DontExportSymbols. NativeLinker relies on these metadata flags to emit --whole-archive, --exclude-libs, etc., so this can change link semantics. Prefer cloning the incoming ITaskItem (e.g., new TaskItem (lib)) and then overriding/setting Abi as needed, so all linker-related metadata is preserved.
| // Build the link items in order: | ||
| // 1. ILC object file | ||
| // 2. Native libraries (.a archives from ILC runtime pack) | ||
| // 3. System libraries (-ldl, -lz, -llog, -lm, -lc) | ||
| // 4. Additional object files (jni_init, environment, etc.) | ||
| // 5. Compiler-rt and unwinder libraries | ||
| var linkItems = new List<ITaskItem> (); | ||
|
|
||
| var nativeObj = new TaskItem (NativeObject.ItemSpec); | ||
| nativeObj.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (nativeObj); | ||
|
|
||
| foreach (var lib in NativeLibraries) { | ||
| var item = new TaskItem (lib.ItemSpec); | ||
| item.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (item); | ||
| } | ||
|
|
||
| if (SystemLibraries != null) { | ||
| foreach (var lib in SystemLibraries) { | ||
| linkItems.Add (NativeLinker.MakeLibraryItem (lib.ItemSpec, abi)); | ||
| } | ||
| } | ||
|
|
||
| if (AdditionalObjectFiles != null) { | ||
| foreach (var obj in AdditionalObjectFiles) { | ||
| var item = new TaskItem (obj.ItemSpec); | ||
| item.SetMetadata (KnownMetadata.Abi, abi); | ||
| linkItems.Add (item); | ||
| } | ||
| } |
There was a problem hiding this comment.
Link item ordering is currently NativeObject + static archives, then -l<system> libraries, and only then additional .o files. With static .a archives and --as-needed, link order affects symbol resolution; object files that reference symbols in earlier archives or libraries may fail to resolve because the linker has already processed them. Consider placing all object files (NativeObject + AdditionalObjectFiles) before static archives, and keeping SystemLibraries after all objects/archives (similar to the ordering logic in LinkNativeRuntime.OrganizeCommandLineItemsCLR).
| public ITaskItem NativeObject { get; set; } = null!; | ||
|
|
||
| /// <summary> | ||
| /// The output shared library path (e.g., libTestApp.so) | ||
| /// </summary> | ||
| [Required] | ||
| public ITaskItem OutputSharedLibrary { get; set; } = null!; |
There was a problem hiding this comment.
The null-forgiving operator (!) is used on [Required] task properties. This can hide nullability issues and makes it easier to accidentally get a NullReferenceException before MSBuild's [Required] validation runs. Prefer non-nullable [Required] properties with safe defaults (or explicit null checks at the start of RunTask()), consistent with other task properties in this file that use = "" / =[].
This issue also appears on line 137 of the same file.
| public ITaskItem NativeObject { get; set; } = null!; | |
| /// <summary> | |
| /// The output shared library path (e.g., libTestApp.so) | |
| /// </summary> | |
| [Required] | |
| public ITaskItem OutputSharedLibrary { get; set; } = null!; | |
| public ITaskItem NativeObject { get; set; } = new TaskItem (""); | |
| /// <summary> | |
| /// The output shared library path (e.g., libTestApp.so) | |
| /// </summary> | |
| [Required] | |
| public ITaskItem OutputSharedLibrary { get; set; } = new TaskItem (""); |
# Conflicts: # src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
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