Skip to content

Commit 0e4c29a

Browse files
authored
[AOT] provide libc and libm stubs so NDK is not required (#7475)
Context: 346a933 Commit 346a933 made it possible to build and link AOT shared libraries without having to have an Android NDK available. However, it seems that the produced shared libraries do not depend on the `libc.so` and `libm.so ` standard libraries, leading to runtime errors similar to: D monodroid-assembly: monodroid_dlopen: hash match found, DSO name is 'libaot-Mono.Android.dll.so' I monodroid-assembly: Trying to load shared library '/data/app/~~4kGTzxWG6HKO7nzyg6ryBg==/com.companyname.hellomaui.old-h8Jlutcvf8Dzfpuuq1ouUA==/lib/arm64/libaot-Mono.Android.dll.so' I monodroid-assembly: Failed to load shared library '/data/app/~~4kGTzxWG6HKO7nzyg6ryBg==/com.companyname.hellomaui.old-h8Jlutcvf8Dzfpuuq1ouUA==/lib/arm64/libaot-Mono.Android.dll.so'. dlopen failed: cannot locate symbol "memset" referenced by "/data/app/~~4kGTzxWG6HKO7nzyg6ryBg==/com.companyname.hellomaui.old-h8Jlutcvf8Dzfpuuq1ouUA==/lib/arm64/libaot-Mono.Android.dll.so"... This is caused by us not passing the `-lc` and `-lm` flags to the linker (or full paths to the `libc.so` and `libm.so` libraries). However, without the NDK available we do not have either of these standard libraries to link against. Fortunately, ELF shared libraries by default will resolve symbols from anywhere in the process address space, so there is no need to provide a `libc.so` which contains *all* public symbols. We can instead build and use a "dummy"/"stub" version of `libc.so` and `libm.so`, existing only so that the linker will add the appropriate `NEEDED` entries and resolve symbols from those libraries. Update `src/monodroid` to also produce "stub" `libc.so` and `libm.so` native libraries, and update the `<Aot/>` task to add the equivalent of `-L path/to/libstubs -lc -lm` to the AOT command-line. This ensures that the resulting `.so` files now require `libc.so`: $ llvm-readelf -a Mono.Android.dll.so | grep NEEDED 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so]
1 parent d174b9b commit 0e4c29a

File tree

8 files changed

+225
-19
lines changed

8 files changed

+225
-19
lines changed

build-tools/installers/create-installers.targets

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@
318318
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)K4os.Compression.LZ4.dll" />
319319
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)ELFSharp.dll" />
320320
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)ManifestOverlays\Timing.xml" />
321+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-arm64\libc.so" />
322+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-arm64\libm.so" />
323+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-arm\libc.so" />
324+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-arm\libm.so" />
325+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-x64\libc.so" />
326+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-x64\libm.so" />
327+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-x86\libc.so" />
328+
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)libstubs\android-x86\libm.so" />
321329
</ItemGroup>
322330
<ItemGroup>
323331
<_MSBuildTargetsSrcFiles Include="$(MSBuildTargetsSrcDir)\Xamarin.Android.AvailableItems.targets" />

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,6 @@ IEnumerable<Config> GetAotConfigs (NdkTools ndk)
169169
if (!string.IsNullOrEmpty (LdName)) {
170170
aotOptions.Add ($"ld-name={LdName}");
171171
}
172-
if (!string.IsNullOrEmpty (LdFlags)) {
173-
aotOptions.Add ($"ld-flags={LdFlags}");
174-
}
175172

176173
// We don't check whether any mode option was added via `AotAdditionalArguments`, the `AndroidAotMode` property should always win here.
177174
// Modes not supported by us directly can be set by setting `AndroidAotMode` to "normal" and adding the desired mode name to the
@@ -186,6 +183,10 @@ IEnumerable<Config> GetAotConfigs (NdkTools ndk)
186183
break;
187184
}
188185

186+
if (!string.IsNullOrEmpty (LdFlags)) {
187+
aotOptions.Add ($"ld-flags={LdFlags}");
188+
}
189+
189190
// we need to quote the entire --aot arguments here to make sure it is parsed
190191
// on windows as one argument. Otherwise it will be split up into multiple
191192
// values, which wont work.

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ string GetLdFlags (NdkTools ndk, AndroidTargetArch arch, int level, string toolP
255255
{
256256
var toolchainPath = toolPrefix.Substring (0, toolPrefix.LastIndexOf (Path.DirectorySeparatorChar));
257257
var ldFlags = new StringBuilder ();
258+
var libs = new List<string> ();
258259
if (UseAndroidNdk && EnableLLVM) {
259260
string androidLibPath = string.Empty;
260261
try {
@@ -273,7 +274,6 @@ string GetLdFlags (NdkTools ndk, AndroidTargetArch arch, int level, string toolP
273274
} else
274275
toolchainLibDir = GetNdkToolchainLibraryDir (ndk, toolchainPath);
275276

276-
var libs = new List<string> ();
277277
if (ndk.UsesClang) {
278278
if (!String.IsNullOrEmpty (toolchainLibDir)) {
279279
libs.Add ($"-L{toolchainLibDir.TrimEnd ('\\')}");
@@ -292,24 +292,36 @@ string GetLdFlags (NdkTools ndk, AndroidTargetArch arch, int level, string toolP
292292
}
293293
libs.Add (Path.Combine (androidLibPath, "libc.so"));
294294
libs.Add (Path.Combine (androidLibPath, "libm.so"));
295+
} else if (!UseAndroidNdk && EnableLLVM) {
296+
// We need to link against libc and libm, but since NDK is not in use, the linker won't be able to find the actual Android libraries.
297+
// Therefore, we will use their stubs to satisfy the linker. At runtime they will, of course, use the actual Android libraries.
298+
string relPath = Path.Combine ("..", "..");
299+
if (!OS.IsWindows) {
300+
// the `binutils` directory is one level down (${OS}/binutils) than the Windows one
301+
relPath = Path.Combine (relPath, "..");
302+
}
303+
string libstubsPath = Path.GetFullPath (Path.Combine (AndroidBinUtilsDirectory, relPath, "libstubs", ArchToRid (arch)));
304+
libs.Add (Path.Combine (libstubsPath, "libc.so"));
305+
libs.Add (Path.Combine (libstubsPath, "libm.so"));
306+
}
295307

308+
if (libs.Count > 0) {
296309
ldFlags.Append ($"\\\"{string.Join ("\\\";\\\"", libs)}\\\"");
297-
} else {
310+
}
311+
312+
//
313+
// This flag is needed for Mono AOT to work correctly with the LLVM 14 `lld` linker due to the following change:
314+
//
315+
// The AArch64 port now supports adrp+ldr and adrp+add optimizations. --no-relax can suppress the optimization.
316+
//
317+
// Without the flag, `lld` will modify AOT-generated code in a way that the Mono runtime doesn't support. Until
318+
// the runtime issue is fixed, we need to pass this flag then.
319+
//
320+
if (!UseAndroidNdk) {
298321
if (ldFlags.Length > 0) {
299322
ldFlags.Append (' ');
300323
}
301-
302-
//
303-
// This flag is needed for Mono AOT to work correctly with the LLVM 14 `lld` linker due to the following change:
304-
//
305-
// The AArch64 port now supports adrp+ldr and adrp+add optimizations. --no-relax can suppress the optimization.
306-
//
307-
// Without the flag, `lld` will modify AOT-generated code in a way that the Mono runtime doesn't support. Until
308-
// the runtime issue is fixed, we need to pass this flag then.
309-
//
310-
if (!UseAndroidNdk) {
311-
ldFlags.Append ("--no-relax");
312-
}
324+
ldFlags.Append ("--no-relax");
313325
}
314326

315327
if (StripLibraries) {
@@ -320,6 +332,26 @@ string GetLdFlags (NdkTools ndk, AndroidTargetArch arch, int level, string toolP
320332
}
321333

322334
return ldFlags.ToString ();
335+
336+
string ArchToRid (AndroidTargetArch arch)
337+
{
338+
switch (arch) {
339+
case AndroidTargetArch.Arm64:
340+
return "android-arm64";
341+
342+
case AndroidTargetArch.Arm:
343+
return "android-arm";
344+
345+
case AndroidTargetArch.X86:
346+
return "android-x86";
347+
348+
case AndroidTargetArch.X86_64:
349+
return "android-x64";
350+
351+
default:
352+
throw new InvalidOperationException ($"Internal error: unsupported ABI '{arch}'");
353+
}
354+
}
323355
}
324356

325357
static string GetNdkToolchainLibraryDir (NdkTools ndk, string binDir, string archDir = null)

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,42 @@ public void CheckMonoComponentsMask (bool enableProfiler, bool useInterpreter, b
8282
}
8383
}
8484

85+
[Test]
86+
public void CheckWhetherLibcAndLibmAreReferencedInAOTLibraries ()
87+
{
88+
var proj = new XamarinAndroidApplicationProject {
89+
IsRelease = true,
90+
EmbedAssembliesIntoApk = true,
91+
AotAssemblies = true,
92+
};
93+
proj.SetProperty ("EnableLLVM", "True");
94+
95+
var abis = new [] { "arm64-v8a", "x86_64" };
96+
proj.SetAndroidSupportedAbis (abis);
97+
98+
var libPaths = new List<string> ();
99+
if (Builder.UseDotNet) {
100+
libPaths.Add (Path.Combine ("android-arm64", "aot", "Mono.Android.dll.so"));
101+
libPaths.Add (Path.Combine ("android-x64", "aot", "Mono.Android.dll.so"));
102+
} else {
103+
libPaths.Add (Path.Combine ("aot", "arm64-v8a", "libaot-Mono.Android.dll.so"));
104+
libPaths.Add (Path.Combine ("aot", "x86_64", "libaot-Mono.Android.dll.so"));
105+
}
106+
107+
using (var b = CreateApkBuilder ()) {
108+
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
109+
string objPath = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
110+
111+
foreach (string libPath in libPaths) {
112+
string lib = Path.Combine (objPath, libPath);
113+
114+
Assert.IsTrue (File.Exists (lib), $"Library {lib} should exist on disk");
115+
Assert.IsTrue (ELFHelper.ReferencesLibrary (lib, "libc.so"), $"Library {lib} should reference libc.so");
116+
Assert.IsTrue (ELFHelper.ReferencesLibrary (lib, "libm.so"), $"Library {lib} should reference libm.so");
117+
}
118+
}
119+
}
120+
85121
static object [] CheckAssemblyCountsSource = new object [] {
86122
new object[] {
87123
/*isRelease*/ false,

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,47 @@ public static bool IsEmptyAOTLibrary (TaskLoggingHelper log, string path)
2828
log.LogWarningFromException (ex, showStackTrace: true);
2929
return false;
3030
}
31+
}
32+
33+
public static bool ReferencesLibrary (string libraryPath, string referencedLibraryName)
34+
{
35+
if (String.IsNullOrEmpty (libraryPath) || !File.Exists (libraryPath)) {
36+
return false;
37+
}
38+
39+
IELF elf = ELFReader.Load (libraryPath);
40+
var dynstr = GetSection (elf, ".dynstr") as IStringTable;
41+
if (dynstr == null) {
42+
return false;
43+
}
44+
45+
foreach (IDynamicSection section in elf.GetSections<IDynamicSection> ()) {
46+
foreach (IDynamicEntry entry in section.Entries) {
47+
if (IsLibraryReference (dynstr, entry, referencedLibraryName)) {
48+
return true;
49+
}
50+
}
51+
}
52+
53+
return false;
54+
}
55+
56+
static bool IsLibraryReference (IStringTable stringTable, IDynamicEntry dynEntry, string referencedLibraryName)
57+
{
58+
if (dynEntry.Tag != DynamicTag.Needed) {
59+
return false;
60+
}
61+
62+
ulong index;
63+
if (dynEntry is DynamicEntry<ulong> entry64) {
64+
index = entry64.Value;
65+
} else if (dynEntry is DynamicEntry<uint> entry32) {
66+
index = (ulong)entry32.Value;
67+
} else {
68+
return false;
69+
}
3170

71+
return String.Compare (referencedLibraryName, stringTable[(long)index], StringComparison.Ordinal) == 0;
3272
}
3373

3474
static bool IsEmptyAOTLibrary (TaskLoggingHelper log, string path, IELF elf)

src/monodroid/CMakeLists.txt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ endif()
427427
if(ANDROID)
428428
if(ENABLE_NET)
429429
set(XA_LIBRARY_OUTPUT_DIRECTORY "${XA_LIB_TOP_DIR}/lib/${ANDROID_RID}")
430+
set(XA_LIBRARY_STUBS_OUTPUT_DIRECTORY "${XA_LIB_TOP_DIR}/libstubs/${ANDROID_RID}")
430431
link_directories("${NET_RUNTIME_DIR}/native")
431432
else()
432433
set(XA_LIBRARY_OUTPUT_DIRECTORY "${XA_LIB_TOP_DIR}/lib/${ANDROID_ABI}")
@@ -570,6 +571,10 @@ set(XAMARIN_DEBUG_APP_HELPER_SOURCES
570571
${SOURCES_DIR}/shared-constants.cc
571572
)
572573

574+
set(XAMARIN_STUB_LIB_SOURCES
575+
libstub/stub.cc
576+
)
577+
573578
# Build
574579
configure_file(jni/host-config.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/host-config.h)
575580

@@ -716,3 +721,57 @@ target_link_libraries(
716721
${XAMARIN_MONO_ANDROID_LIB}
717722
${LINK_LIBS} xamarin-app
718723
)
724+
725+
if(ANDROID AND ENABLE_NET)
726+
add_library(
727+
c
728+
SHARED ${XAMARIN_STUB_LIB_SOURCES}
729+
)
730+
731+
target_compile_definitions(
732+
c
733+
PRIVATE STUB_LIB_NAME=libc
734+
)
735+
736+
target_compile_options(
737+
c
738+
PRIVATE -nostdlib
739+
)
740+
741+
target_link_options(
742+
c
743+
PRIVATE -nostdlib
744+
)
745+
746+
set_target_properties(
747+
c
748+
PROPERTIES
749+
LIBRARY_OUTPUT_DIRECTORY "${XA_LIBRARY_STUBS_OUTPUT_DIRECTORY}"
750+
)
751+
752+
add_library(
753+
m
754+
SHARED ${XAMARIN_STUB_LIB_SOURCES}
755+
)
756+
757+
target_compile_definitions(
758+
m
759+
PRIVATE STUB_LIB_NAME=libm
760+
)
761+
762+
target_compile_options(
763+
m
764+
PRIVATE -nostdlib
765+
)
766+
767+
target_link_options(
768+
m
769+
PRIVATE -nostdlib
770+
)
771+
772+
set_target_properties(
773+
m
774+
PROPERTIES
775+
LIBRARY_OUTPUT_DIRECTORY "${XA_LIBRARY_STUBS_OUTPUT_DIRECTORY}"
776+
)
777+
endif()

src/monodroid/libstub/stub.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#if !defined (STUB_LIB_NAME)
2+
#error STUB_LIB_NAME must be defined on command line
3+
#endif
4+
5+
void STUB_LIB_NAME ()
6+
{
7+
// no-op
8+
}

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,30 @@ public void RunWithLLVMEnabled ()
694694
Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");
695695
RunProjectAndAssert (proj, builder);
696696

697-
Assert.IsTrue (WaitForActivityToStart (proj.PackageName, "MainActivity",
698-
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log")));
697+
var activityNamespace = proj.PackageName;
698+
var activityName = "MainActivity";
699+
var logcatFilePath = Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log");
700+
var failedToLoad = new List<string> ();
701+
bool appLaunched = MonitorAdbLogcat ((line) => {
702+
if (SeenFailedToLoad (line))
703+
failedToLoad.Add (line);
704+
return SeenActivityDisplayed (line);
705+
}, logcatFilePath, timeout: 120);
706+
707+
Assert.IsTrue (appLaunched, "LLVM app did not launch");
708+
Assert.AreEqual (0, failedToLoad.Count, $"LLVM .so files not loaded:\n{string.Join ("\n", failedToLoad)}");
709+
710+
bool SeenActivityDisplayed (string line)
711+
{
712+
var idx1 = line.IndexOf ("ActivityManager: Displayed", StringComparison.OrdinalIgnoreCase);
713+
var idx2 = idx1 > 0 ? 0 : line.IndexOf ("ActivityTaskManager: Displayed", StringComparison.OrdinalIgnoreCase);
714+
return (idx1 > 0 || idx2 > 0) && line.Contains (activityNamespace) && line.Contains (activityName);
715+
}
716+
717+
bool SeenFailedToLoad (string line)
718+
{
719+
return line.Contains ("Failed to load shared library");
720+
}
699721
}
700722

701723
[Test]

0 commit comments

Comments
 (0)