Skip to content

Commit 69c5e63

Browse files
committed
Actually linking now
1 parent 815ae8d commit 69c5e63

File tree

6 files changed

+206
-24
lines changed

6 files changed

+206
-24
lines changed

NativeLinkingTODO.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Problems to solve
2+
3+
* `libSystem.Security.Cryptography.Native.Android.a` contains the `JNI_OnLoad` function
4+
which initializes the whole crypto support library, but we can't use it as it would
5+
conflict with our own. Potential solution is to modify the above library's source code
6+
to add an init function that we will call from our own `JNI_OnLoad` and make the library's
7+
init function do the same. The `JNI_OnLoad` object file would have to be omitted from the
8+
library's `.a`
9+
* `p/invoke usage`.
10+
Currently, all the BCL archives (with exception of the above crypto one) are
11+
linked into the unified runtime using `--whole-archive` - that is, they become part of the
12+
runtime in their entirety. This is wasteful, but necessary, so that `p/invokes` into those
13+
libraries work correctly. Instead, we should scan the application DLLs for p/invokes from
14+
those libraries and generate code to reference the required functions, so that the linker
15+
can do its job and remove code not used by the application. Likely needed is a linker step.
16+
* `p/invoke` handling mechanism. Right now, we `dlopen` the relevant `.so` library and look
17+
up the required symbol in there. With the unified runtime the `.so` disappears, so we either
18+
need to look it up in our own library or, better, call the function directly. The latter is
19+
a bit more complicated to implement but would give us much faster code, thus it's the preferred
20+
solution.
21+
22+
# Ideas
23+
24+
* Use [mold](https://github.com/rui314/mold) which has recently been re-licensed under `MIT/X11`
25+
(and contains components licensed under a mixture of `BSD*` and `Apache 2.0` licenses), so we
26+
can easily redistribute it instead of the LLVM's `lld`. The advantage is `mold`'s [speed](https://github.com/rui314/mold?tab=readme-ov-file#mold-a-modern-linker)

src/Xamarin.Android.Build.Tasks/Tasks/GetNativeRuntimeComponents.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void MakeLibraryItems (string libName, List<ITaskItem> libraries, HashSet<string
4949
{
5050
foreach (string abi in uniqueAbis) {
5151
var item = new TaskItem (libName);
52-
item.SetMetadata ("Abi", abi);
52+
item.SetMetadata (KnownMetadata.Abi, abi);
5353
libraries.Add (item);
5454
}
5555
}
@@ -68,7 +68,8 @@ ITaskItem DoMakeItem (ITaskItem resolved)
6868
var ret = new TaskItem (resolved.ItemSpec);
6969
string abi = MonoAndroidHelper.RidToAbi (resolved.GetRequiredMetadata ("_ResolvedNativeArchive", "RuntimeIdentifier", Log));
7070
uniqueAbis.Add (abi);
71-
ret.SetMetadata ("Abi", abi);
71+
ret.SetMetadata (KnownMetadata.Abi, abi);
72+
ret.SetMetadata (KnownMetadata.LinkWholeArchive, archive.WholeArchive.ToString ());
7273

7374
return ret;
7475
}

src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeRuntime.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ void LinkRuntime (ITaskItem abiItem)
5151
soname = soname.Substring (3);
5252
}
5353

54-
var linker = new NativeLinker (Log, abi, soname, AndroidBinUtilsDirectory, IntermediateOutputPath);
54+
var linker = new NativeLinker (Log, abi, soname, AndroidBinUtilsDirectory, IntermediateOutputPath, CancellationToken, Cancel);
5555
linker.Link (
5656
outputRuntime,
5757
GetAbiItems (NativeObjectFiles, "_NativeAssemblyTarget", abi),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Xamarin.Android.Tasks;
2+
3+
static class KnownMetadata
4+
{
5+
public const string Abi = "Abi";
6+
public const string LinkWholeArchive = "LinkWholeArchive";
7+
}

src/Xamarin.Android.Build.Tasks/Utilities/NativeLinker.cs

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Diagnostics;
23
using System.IO;
34
using System.Collections.Generic;
45
using System.Text;
6+
using System.Threading;
57

68
using Microsoft.Build.Framework;
79
using Microsoft.Build.Utilities;
@@ -13,9 +15,9 @@ class NativeLinker
1315
{
1416
static readonly List<string> standardArgs = new () {
1517
"--shared",
16-
"--as-needed",
1718
"--allow-shlib-undefined",
18-
"--compress-debug-sections",
19+
// TODO: need to enable zstd in binutils build
20+
// "--compress-debug-sections=zstd",
1921
// TODO: test the commented-out flags
2022
// "--gc-sections",
2123
// "--icf=safe",
@@ -34,15 +36,20 @@ class NativeLinker
3436
readonly string abi;
3537
readonly string ld;
3638
readonly string intermediateDir;
39+
readonly CancellationToken? cancellationToken;
40+
readonly Action? cancelTask;
3741

3842
public bool StripDebugSymbols { get; set; }
3943
public bool SaveDebugSymbols { get; set; }
4044

41-
public NativeLinker (TaskLoggingHelper log, string abi, string soname, string binutilsDir, string intermediateDir)
45+
public NativeLinker (TaskLoggingHelper log, string abi, string soname, string binutilsDir, string intermediateDir,
46+
CancellationToken? cancellationToken = null, Action? cancelTask = null)
4247
{
4348
this.log = log;
4449
this.abi = abi;
4550
this.intermediateDir = intermediateDir;
51+
this.cancellationToken = cancellationToken;
52+
this.cancelTask = cancelTask;
4653

4754
ld = Path.Combine (binutilsDir, MonoAndroidHelper.GetExecutablePath (binutilsDir, "ld"));
4855

@@ -77,17 +84,29 @@ public NativeLinker (TaskLoggingHelper log, string abi, string soname, string bi
7784
if (!String.IsNullOrEmpty (elfArch)) {
7885
extraArgs.Add ($"-m {elfArch}");
7986
}
87+
88+
string runtimeNativeLibsDir = MonoAndroidHelper.GetNativeLibsRootDirectoryPath (binutilsDir);
89+
string runtimeNativeLibStubsDir = MonoAndroidHelper.GetLibstubsRootDirectoryPath (binutilsDir);
90+
string RID = MonoAndroidHelper.AbiToRid (abi);
91+
string libStubsPath = Path.Combine (runtimeNativeLibStubsDir, RID);
92+
string runtimeLibsDir = Path.Combine (runtimeNativeLibsDir, RID);
93+
94+
extraArgs.Add ($"-L {MonoAndroidHelper.QuoteFileNameArgument (libStubsPath)}");
95+
extraArgs.Add ($"-L {MonoAndroidHelper.QuoteFileNameArgument (runtimeLibsDir)}");
8096
}
8197

82-
public void Link (ITaskItem outputLibraryPath, List<ITaskItem> objectFiles, List<ITaskItem> archives, List<ITaskItem> libraries)
98+
public bool Link (ITaskItem outputLibraryPath, List<ITaskItem> objectFiles, List<ITaskItem> archives, List<ITaskItem> libraries)
8399
{
84100
log.LogDebugMessage ($"Linking: {outputLibraryPath}");
85101
EnsureCorrectAbi (outputLibraryPath);
86102
EnsureCorrectAbi (objectFiles);
87103
EnsureCorrectAbi (archives);
88104
EnsureCorrectAbi (libraries);
89105

90-
string respFilePath = Path.Combine (intermediateDir, $"ld.{abi}.rsp");
106+
Directory.CreateDirectory (Path.GetDirectoryName (outputLibraryPath.ItemSpec));
107+
108+
string libBaseName = Path.GetFileNameWithoutExtension (outputLibraryPath.ItemSpec);
109+
string respFilePath = Path.Combine (intermediateDir, $"ld.{libBaseName}.{abi}.rsp");
91110
using (var sw = new StreamWriter (File.Open (respFilePath, FileMode.Create, FileAccess.Write, FileShare.Read), new UTF8Encoding (false))) {
92111
foreach (string arg in standardArgs) {
93112
sw.WriteLine (arg);
@@ -101,29 +120,62 @@ public void Link (ITaskItem outputLibraryPath, List<ITaskItem> objectFiles, List
101120
sw.WriteLine ("-s");
102121
}
103122

104-
sw.Write ("-o ");
105-
sw.WriteLine (MonoAndroidHelper.QuoteFileNameArgument (outputLibraryPath.ItemSpec));
106-
107123
WriteFilesToResponseFile (sw, objectFiles);
108124
WriteFilesToResponseFile (sw, archives);
109125

126+
foreach (ITaskItem libItem in libraries) {
127+
sw.WriteLine ($"-l{libItem.ItemSpec}");
128+
}
129+
110130
sw.Flush ();
111131
}
112132

113-
log.LogDebugMessage ($" Command line: {ld} @{respFilePath}");
133+
var ldArgs = new List<string> {
134+
$"@{respFilePath}",
135+
"-o",
136+
MonoAndroidHelper.QuoteFileNameArgument (outputLibraryPath.ItemSpec)
137+
};
138+
139+
var watch = new Stopwatch ();
140+
watch.Start ();
141+
bool ret = RunLinker (ldArgs, outputLibraryPath);
142+
watch.Stop ();
143+
log.LogDebugMessage ($"[{Path.GetFileName (outputLibraryPath.ItemSpec)} link time] {watch.Elapsed}");
144+
145+
return ret;
114146

115147
void WriteFilesToResponseFile (StreamWriter sw, List<ITaskItem> files)
116148
{
117149
foreach (ITaskItem file in files) {
118-
sw.WriteLine (MonoAndroidHelper.QuoteFileNameArgument (file.ItemSpec));
150+
bool wholeArchive = IncludeWholeArchive (file);
151+
152+
if (wholeArchive) {
153+
sw.Write ("--whole-archive ");
154+
}
155+
sw.Write (MonoAndroidHelper.QuoteFileNameArgument (file.ItemSpec));
156+
if (wholeArchive) {
157+
sw.Write (" --no-whole-archive");
158+
}
159+
sw.WriteLine ();
160+
}
161+
}
162+
163+
bool IncludeWholeArchive (ITaskItem item)
164+
{
165+
string? wholeArchive = item.GetMetadata (KnownMetadata.LinkWholeArchive);
166+
if (String.IsNullOrEmpty (wholeArchive)) {
167+
return false;
119168
}
169+
170+
// Purposefully not calling TryParse, let it throw and let us know if the value isn't a boolean.
171+
return Boolean.Parse (wholeArchive);
120172
}
121173
}
122174

123175
void EnsureCorrectAbi (ITaskItem item)
124176
{
125177
// The exception is just a precaution, since the items passed to us should have already been checked
126-
string itemAbi = item.GetMetadata ("Abi") ?? throw new InvalidOperationException ($"Internal error: 'Abi' metadata not found in item '{item}'");
178+
string itemAbi = item.GetMetadata (KnownMetadata.Abi) ?? throw new InvalidOperationException ($"Internal error: 'Abi' metadata not found in item '{item}'");
127179
if (String.Compare (abi, itemAbi, StringComparison.OrdinalIgnoreCase) == 0) {
128180
return;
129181
}
@@ -137,4 +189,82 @@ void EnsureCorrectAbi (List<ITaskItem> items)
137189
EnsureCorrectAbi (item);
138190
}
139191
}
192+
193+
bool RunLinker (List<string> args, ITaskItem outputSharedLibrary)
194+
{
195+
using var stdout_completed = new ManualResetEvent (false);
196+
using var stderr_completed = new ManualResetEvent (false);
197+
var psi = new ProcessStartInfo () {
198+
FileName = ld,
199+
Arguments = String.Join (" ", args),
200+
UseShellExecute = false,
201+
RedirectStandardOutput = true,
202+
RedirectStandardError = true,
203+
CreateNoWindow = true,
204+
WindowStyle = ProcessWindowStyle.Hidden,
205+
};
206+
207+
string linkerName = Path.GetFileName (ld);
208+
log.LogDebugMessage ($"[Native Linker] {psi.FileName} {psi.Arguments}");
209+
210+
var stdoutLines = new List<string> ();
211+
var stderrLines = new List<string> ();
212+
213+
using var proc = new Process ();
214+
proc.OutputDataReceived += (s, e) => {
215+
if (e.Data != null) {
216+
OnOutputData (linkerName, s, e);
217+
stdoutLines.Add (e.Data);
218+
} else {
219+
stdout_completed.Set ();
220+
}
221+
};
222+
223+
proc.ErrorDataReceived += (s, e) => {
224+
if (e.Data != null) {
225+
OnErrorData (linkerName, s, e);
226+
stderrLines.Add (e.Data);
227+
} else {
228+
stderr_completed.Set ();
229+
}
230+
};
231+
232+
proc.StartInfo = psi;
233+
proc.Start ();
234+
proc.BeginOutputReadLine ();
235+
proc.BeginErrorReadLine ();
236+
cancellationToken?.Register (() => { try { proc.Kill (); } catch (Exception) { } });
237+
proc.WaitForExit ();
238+
239+
if (psi.RedirectStandardError) {
240+
stderr_completed.WaitOne (TimeSpan.FromSeconds (30));
241+
}
242+
243+
if (psi.RedirectStandardOutput) {
244+
stdout_completed.WaitOne (TimeSpan.FromSeconds (30));
245+
}
246+
247+
if (proc.ExitCode != 0) {
248+
var sb = MonoAndroidHelper.MergeStdoutAndStderrMessages (stdoutLines, stderrLines);
249+
log.LogCodedError ("XA3007", Properties.Resources.XA3007, Path.GetFileName (outputSharedLibrary.ItemSpec), sb.ToString ());
250+
cancelTask?.Invoke ();
251+
return false;
252+
}
253+
254+
return true;
255+
}
256+
257+
void OnOutputData (string linkerName, object sender, DataReceivedEventArgs e)
258+
{
259+
if (e.Data != null) {
260+
log.LogMessage ($"[{linkerName} stdout] {e.Data}");
261+
}
262+
}
263+
264+
void OnErrorData (string linkerName, object sender, DataReceivedEventArgs e)
265+
{
266+
if (e.Data != null) {
267+
log.LogMessage ($"[{linkerName} stderr] {e.Data}");
268+
}
269+
}
140270
}

src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ internal class Archive
1111
{
1212
public readonly string Name;
1313
public bool Include => shouldInclude (this);
14+
public readonly bool WholeArchive;
1415

1516
Func<Archive, bool> shouldInclude;
1617

17-
public Archive (string name, Func<Archive, bool>? include = null)
18+
public Archive (string name, Func<Archive, bool>? include = null, bool wholeArchive = false)
1819
{
1920
Name = name;
2021
shouldInclude = include == null ? ((Archive arch) => true) : include;
22+
WholeArchive = wholeArchive;
2123
}
2224
}
2325

@@ -32,6 +34,20 @@ public MonoComponentArchive (string name, string componentName, Func<Archive, bo
3234
}
3335
}
3436

37+
class AndroidArchive : Archive
38+
{
39+
public AndroidArchive (string name)
40+
: base (name, wholeArchive: true)
41+
{}
42+
}
43+
44+
class BclArchive : Archive
45+
{
46+
public BclArchive (string name, bool wholeArchive = true)
47+
: base (name, wholeArchive: wholeArchive)
48+
{}
49+
}
50+
3551
readonly ITaskItem[] monoComponents;
3652

3753
public readonly List<Archive> KnownArchives;
@@ -49,17 +65,19 @@ public NativeRuntimeComponents (ITaskItem[] monoComponents)
4965

5066
// MonoVM runtime + BCL
5167
new Archive ("libmonosgen-2.0.a"),
52-
new Archive ("libSystem.Globalization.Native.a"),
53-
new Archive ("libSystem.IO.Compression.Native.a"),
54-
new Archive ("libSystem.Native.a"),
55-
new Archive ("libSystem.Security.Cryptography.Native.Android.a"),
68+
new BclArchive ("libSystem.Globalization.Native.a"),
69+
new BclArchive ("libSystem.IO.Compression.Native.a"),
70+
new BclArchive ("libSystem.Native.a"),
71+
72+
// Can't link whole archive for this one because it contains conflicting JNI_OnLoad
73+
new BclArchive ("libSystem.Security.Cryptography.Native.Android.a", wholeArchive: false),
5674

5775
// .NET for Android
58-
new Archive ("libruntime-base.a"),
59-
new Archive ("libxa-java-interop.a"),
60-
new Archive ("libxa-lz4.a"),
61-
new Archive ("libxa-shared-bits.a"),
62-
new Archive ("libmono-android.release-static.a"),
76+
new AndroidArchive ("libruntime-base.a"),
77+
new AndroidArchive ("libxa-java-interop.a"),
78+
new AndroidArchive ("libxa-lz4.a"),
79+
new AndroidArchive ("libxa-shared-bits.a"),
80+
new AndroidArchive ("libmono-android.release-static.a"),
6381
};
6482

6583
// Just the base names of libraries to link into the unified runtime. Must have all the dependencies of all the static archives we

0 commit comments

Comments
 (0)