Skip to content

[NativeAOT] Use NativeLinker and invoke lld directly for linking#11256

Open
sbomer wants to merge 11 commits intomainfrom
dev/sbomer/nativelink-task
Open

[NativeAOT] Use NativeLinker and invoke lld directly for linking#11256
sbomer wants to merge 11 commits intomainfrom
dev/sbomer/nativelink-task

Conversation

@sbomer
Copy link
Copy Markdown
Member

@sbomer sbomer commented Apr 30, 2026

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

sbomer and others added 10 commits April 17, 2026 10:27
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>
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>
Copilot AI review requested due to automatic review settings April 30, 2026 17:33
@sbomer sbomer changed the title Use NativeLinker for Native AOT linking [NativeAOT] Use NativeLinker and invoke lld directly for linking Apr 30, 2026
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 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 NativeLinker with NativeAOT-focused linker options (export-dynamic, version scripts, linker scripts, debug section compression, extra -L paths, etc.).
  • Add a new MSBuild task (LinkNativeAotSharedLibrary) that drives NativeLinker to produce a .so from ILC output + runtime/static archives.
  • Update Microsoft.Android.Sdk.NativeAOT.targets to set NativeLib=static, enable unmanaged entrypoint exports, and run the new link step after LinkNative.
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 if LinkerScript is just a filename (no directory component), because Path.GetDirectoryName() would return null. Avoid the null-forgiving operator here and handle the null case explicitly (or use IntermediateOutputPath as 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,
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
SaveDebugSymbols = true,
SaveDebugSymbols = !DebugBuild,

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +191
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);
}
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +183
// 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);
}
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
public ITaskItem NativeObject { get; set; } = null!;

/// <summary>
/// The output shared library path (e.g., libTestApp.so)
/// </summary>
[Required]
public ITaskItem OutputSharedLibrary { get; set; } = null!;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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 ("");

Copilot uses AI. Check for mistakes.
# Conflicts:
#	src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
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.

2 participants