Skip to content

Commit bb04b1e

Browse files
committed
Assembly source generation
1 parent c3b3e89 commit bb04b1e

File tree

7 files changed

+267
-38
lines changed

7 files changed

+267
-38
lines changed

samples/NativeAOT/run.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ if [ "${FAILED}" == "yes" ]; then
1212
exit 1
1313
fi
1414

15+
if [ -n "${1}" ]; then
16+
exit 0
17+
fi
18+
1519
adb uninstall "${PACKAGE}" || true
1620
adb install -r -d --no-streaming --no-fastdeploy "${APK}"
1721
adb shell setprop debug.mono.log default,assembly,timing=bare

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.
88
<Project>
99

1010
<UsingTask TaskName="Xamarin.Android.Tasks.SetNdkPathForIlc" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
11+
<UsingTask TaskName="Xamarin.Android.Tasks.GenerateNativeAotLibraryLoadAssemblerSources" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
1112

1213
<!-- Default property values for NativeAOT -->
1314
<PropertyGroup>
@@ -122,6 +123,37 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.
122123
</ItemGroup>
123124
</Target>
124125

126+
<!-- Make sure we have a chance to generate our application-specific static library before linking
127+
takes place -->
128+
<PropertyGroup>
129+
<!-- This should work, but unfortunately there's a bug in the current NAOT targets which
130+
overwrites `LinkNativeDependsOn` property value -->
131+
<LinkNativeDependsOn>
132+
_GenerateNativeAotAndroidAppLibrary;
133+
$(LinkNativeDependsOn)
134+
</LinkNativeDependsOn>
135+
</PropertyGroup>
136+
137+
<Target
138+
Name="_GenerateNativeAotAndroidAppAssemblerSources">
139+
<ItemGroup>
140+
<_PrivateAndroidNaotResolvedAssemblyFiles Include="@(ResolvedFileToPublish->Distinct())" Condition=" '%(ResolvedFileToPublish.Extension)' == '.dll' " />
141+
</ItemGroup>
142+
143+
<GenerateNativeAotLibraryLoadAssemblerSources
144+
ResolvedAssemblies="@(_PrivateAndroidNaotResolvedAssemblyFiles)"
145+
CustomJniInitFunctions="@(AndroidNativeAotJniInitFunction)"
146+
SourcesOutputDirectory="$(IntermediateOutputPath)android" />
147+
148+
<!-- TODO: we need environment files here too -->
149+
</Target>
150+
151+
<Target
152+
Name="_GenerateNativeAotAndroidAppLibrary"
153+
AfterTargets="IlcCompile"
154+
DependsOnTargets="_GenerateNativeAotAndroidAppAssemblerSources">
155+
</Target>
156+
125157
<Target Name="_AndroidFixNativeLibraryFileName" AfterTargets="ComputeFilesToPublish">
126158
<ItemGroup>
127159
<!-- Fix paths to contain lib-prefix -->

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

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -91,32 +91,6 @@ public override bool RunTask ()
9191
return !Log.HasLoggedErrors;
9292
}
9393

94-
XAAssemblyResolver MakeResolver (bool useMarshalMethods, AndroidTargetArch targetArch, Dictionary<string, ITaskItem> assemblies)
95-
{
96-
var readerParams = new ReaderParameters ();
97-
if (useMarshalMethods) {
98-
readerParams.ReadWrite = true;
99-
readerParams.InMemory = true;
100-
}
101-
102-
var res = new XAAssemblyResolver (targetArch, Log, loadDebugSymbols: true, loadReaderParameters: readerParams);
103-
var uniqueDirs = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
104-
105-
Log.LogDebugMessage ($"Adding search directories to new architecture {targetArch} resolver:");
106-
foreach (var kvp in assemblies) {
107-
string assemblyDir = Path.GetDirectoryName (kvp.Value.ItemSpec);
108-
if (uniqueDirs.Contains (assemblyDir)) {
109-
continue;
110-
}
111-
112-
uniqueDirs.Add (assemblyDir);
113-
res.SearchDirectories.Add (assemblyDir);
114-
Log.LogDebugMessage ($" {assemblyDir}");
115-
}
116-
117-
return res;
118-
}
119-
12094
void Run (bool useMarshalMethods)
12195
{
12296
PackageNamingPolicy pnp;
@@ -253,7 +227,7 @@ internal static Dictionary<string, ITaskItem> MaybeGetArchAssemblies (Dictionary
253227

254228
(bool success, NativeCodeGenState? stubsState) GenerateJavaSourcesAndMaybeClassifyMarshalMethods (AndroidTargetArch arch, Dictionary<string, ITaskItem> assemblies, Dictionary<string, ITaskItem> userAssemblies, bool useMarshalMethods, bool generateJavaCode)
255229
{
256-
XAAssemblyResolver resolver = MakeResolver (useMarshalMethods, arch, assemblies);
230+
XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods, arch, assemblies);
257231
var tdCache = new TypeDefinitionCache ();
258232
(List<TypeDefinition> allJavaTypes, List<TypeDefinition> javaTypesForJCW) = ScanForJavaTypes (resolver, tdCache, assemblies, userAssemblies, useMarshalMethods);
259233
var jcwContext = new JCWGeneratorContext (arch, resolver, assemblies.Values, javaTypesForJCW, tdCache);
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
5+
using Microsoft.Android.Build.Tasks;
6+
using Microsoft.Build.Framework;
7+
using Xamarin.Android.Build.Tasks;
8+
using Xamarin.Android.Tools;
9+
10+
namespace Xamarin.Android.Tasks;
11+
12+
public class GenerateNativeAotLibraryLoadAssemblerSources : AndroidTask
13+
{
14+
static readonly HashSet<string> KnownLibraryExtensions = new (StringComparer.OrdinalIgnoreCase) {
15+
".so",
16+
".dll",
17+
".dylib",
18+
};
19+
20+
static readonly HashSet<string> LibraryNamesToIgnore = new (StringComparer.OrdinalIgnoreCase) {
21+
"*",
22+
"android",
23+
"c",
24+
"libandroid",
25+
"libandroid.so",
26+
"libc",
27+
"libc.dylib",
28+
"libc.so",
29+
"liblog",
30+
"liblog.so",
31+
"log",
32+
"xa-internal-api",
33+
};
34+
35+
public override string TaskPrefix => "GNALLAS";
36+
37+
[Required]
38+
public ITaskItem[] ResolvedAssemblies { get; set; } = [];
39+
40+
[Required]
41+
public string SourcesOutputDirectory { get; set; } = "";
42+
43+
// Names of JNI initialization functions in 3rd party libraries. The
44+
// functions are REQUIRED to use the `JNI_OnLoad(JNIEnv*, void* reserved)` signature.
45+
// TODO: document it in `Documentation/`
46+
public ITaskItem[] CustomJniInitFunctions { get; set; } = [];
47+
48+
public override bool RunTask ()
49+
{
50+
if (ResolvedAssemblies.Length == 0) {
51+
return true;
52+
}
53+
54+
// We run in the inner build, there's going to be just a single RID
55+
string rid = MonoAndroidHelper.GetAssemblyRid (ResolvedAssemblies[0]);
56+
AndroidTargetArch targetArch = MonoAndroidHelper.RidToArch (rid);
57+
58+
var assemblies = new Dictionary<string, ITaskItem> (StringComparer.OrdinalIgnoreCase);
59+
foreach (ITaskItem item in ResolvedAssemblies) {
60+
string name = MonoAndroidHelper.GetAssemblyNameWithCulture (item);
61+
if (assemblies.ContainsKey (name)) {
62+
continue;
63+
}
64+
65+
assemblies.Add (name, item);
66+
}
67+
68+
XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods: false, targetArch, assemblies, loadDebugSymbols: false);
69+
var pinvokeScanner = new PinvokeScanner (Log, debugLogging: false);
70+
List<PinvokeScanner.PinvokeEntryInfo> pinfos = pinvokeScanner.Scan (targetArch, resolver, ResolvedAssemblies);
71+
72+
var seen = new HashSet<string> (LibraryNamesToIgnore, StringComparer.OrdinalIgnoreCase);
73+
var pinvokeLibraries = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
74+
foreach (PinvokeScanner.PinvokeEntryInfo pinfo in pinfos) {
75+
if (seen.Contains (pinfo.LibraryName)) {
76+
continue;
77+
}
78+
79+
seen.Add (pinfo.LibraryName);
80+
81+
// All the BCL libraries follow the standard naming pattern of `lib<NAME>`, make sure that's what we have
82+
pinvokeLibraries.Add (MakeCanonicalLibraryName (pinfo.LibraryName));
83+
}
84+
85+
// Take library names, match against NativeRuntimeComponents to see whether a
86+
// component has an init function associated with it.
87+
var bclComponents = new NativeRuntimeComponents (monoComponents: null);
88+
var bclInitFunctions = new List<string> ();
89+
90+
seen = new HashSet<string> (StringComparer.Ordinal);
91+
foreach (string lib in pinvokeLibraries) {
92+
foreach (NativeRuntimeComponents.Archive archive in bclComponents.KnownArchives) {
93+
if (lib != archive.Name) {
94+
continue;
95+
}
96+
97+
if (String.IsNullOrEmpty (archive.JniOnLoadName)) {
98+
continue;
99+
}
100+
101+
if (seen.Contains (archive.JniOnLoadName!)) {
102+
continue;
103+
}
104+
105+
seen.Add (archive.JniOnLoadName!);
106+
bclInitFunctions.Add (archive.JniOnLoadName!);
107+
}
108+
}
109+
110+
if (bclInitFunctions.Count > 0) {
111+
Log.LogDebugMessage ("Found BCL JNI init functions to call on application init:");
112+
foreach (string func in bclInitFunctions) {
113+
Log.LogDebugMessage ($" {func}");
114+
}
115+
}
116+
117+
List<string>? customInitFunctions = null;
118+
if (CustomJniInitFunctions != null && CustomJniInitFunctions.Length > 0) {
119+
customInitFunctions = new List<string> ();
120+
seen.Clear ();
121+
Log.LogDebugMessage ("Custom JNI init functions to call on application init:");
122+
foreach (ITaskItem func in CustomJniInitFunctions) {
123+
string name = func.ItemSpec;
124+
if (seen.Contains (name)) {
125+
continue;
126+
}
127+
seen.Add (name);
128+
customInitFunctions.Add (name);
129+
Log.LogDebugMessage ($" {name}");
130+
}
131+
}
132+
133+
var jniInitFuncsLlFilePath = Path.Combine (SourcesOutputDirectory, $"jni_init_funcs.{MonoAndroidHelper.RidToAbi (rid)}.ll");
134+
var generator = new NativeAotDsoLoadNativeAssemblyGenerator (Log, bclInitFunctions, customInitFunctions);
135+
LLVMIR.LlvmIrModule jniInitFuncsModule = generator.Construct ();
136+
using var jniInitFuncsWriter = MemoryStreamPool.Shared.CreateStreamWriter ();
137+
bool fileFullyWritten = false;
138+
try {
139+
generator.Generate (jniInitFuncsModule, targetArch, jniInitFuncsWriter, jniInitFuncsLlFilePath!);
140+
jniInitFuncsWriter.Flush ();
141+
Files.CopyIfStreamChanged (jniInitFuncsWriter.BaseStream, jniInitFuncsLlFilePath!);
142+
fileFullyWritten = true;
143+
} finally {
144+
// Log partial contents for debugging if generation failed
145+
if (!fileFullyWritten) {
146+
MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{jniInitFuncsLlFilePath}'", jniInitFuncsWriter.BaseStream);
147+
}
148+
}
149+
return !Log.HasLoggedErrors;
150+
}
151+
152+
static string MakeCanonicalLibraryName (string libName)
153+
{
154+
string? ext = Path.GetExtension (libName);
155+
if (!String.IsNullOrEmpty (ext) && KnownLibraryExtensions.Contains (ext)) {
156+
libName = Path.GetFileNameWithoutExtension (libName);
157+
}
158+
159+
if (!libName.StartsWith ("lib", StringComparison.OrdinalIgnoreCase)) {
160+
libName = $"lib{libName}";
161+
}
162+
163+
// We will be matching against list of static archives, add the correct extension
164+
return $"{libName}.a";
165+
}
166+
}

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Xamarin.Android.Tools;
1313
using Xamarin.Tools.Zip;
1414
using Java.Interop.Tools.JavaCallableWrappers;
15+
using Mono.Cecil;
16+
1517

1618
#if MSBUILD
1719
using Microsoft.Android.Build.Tasks;
@@ -30,6 +32,32 @@ public partial class MonoAndroidHelper
3032
public static AndroidVersions SupportedVersions;
3133
public static AndroidSdkInfo AndroidSdk;
3234

35+
internal static XAAssemblyResolver MakeResolver (TaskLoggingHelper log, bool useMarshalMethods, AndroidTargetArch targetArch, Dictionary<string, ITaskItem> assemblies, bool loadDebugSymbols = true)
36+
{
37+
var readerParams = new ReaderParameters ();
38+
if (useMarshalMethods) {
39+
readerParams.ReadWrite = true;
40+
readerParams.InMemory = true;
41+
}
42+
43+
var res = new XAAssemblyResolver (targetArch, log, loadDebugSymbols: loadDebugSymbols, loadReaderParameters: readerParams);
44+
var uniqueDirs = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
45+
46+
log.LogDebugMessage ($"Adding search directories to new architecture {targetArch} resolver:");
47+
foreach (var kvp in assemblies) {
48+
string assemblyDir = Path.GetDirectoryName (kvp.Value.ItemSpec);
49+
if (uniqueDirs.Contains (assemblyDir)) {
50+
continue;
51+
}
52+
53+
uniqueDirs.Add (assemblyDir);
54+
res.SearchDirectories.Add (assemblyDir);
55+
log.LogDebugMessage ($" {assemblyDir}");
56+
}
57+
58+
return res;
59+
}
60+
3361
public static StringBuilder MergeStdoutAndStderrMessages (List<string> stdout, List<string> stderr)
3462
{
3563
var sb = new StringBuilder ();
@@ -578,6 +606,16 @@ public static string GetAssemblyAbi (ITaskItem asmItem)
578606
return abi;
579607
}
580608

609+
public static string GetAssemblyRid (ITaskItem asmItem)
610+
{
611+
string? abi = asmItem.GetMetadata ("RuntimeIdentifier");
612+
if (String.IsNullOrEmpty (abi)) {
613+
throw new InvalidOperationException ($"Internal error: assembly '{asmItem}' lacks RuntimeIdentifier metadata");
614+
}
615+
616+
return abi;
617+
}
618+
581619
public static AndroidTargetArch GetTargetArch (ITaskItem asmItem) => AbiToTargetArch (GetAssemblyAbi (asmItem));
582620

583621

@@ -681,6 +719,16 @@ public static Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> GetPe
681719
);
682720
}
683721

722+
public static string GetAssemblyNameWithCulture (ITaskItem assemblyItem)
723+
{
724+
string name = Path.GetFileNameWithoutExtension (assemblyItem.ItemSpec);
725+
string? culture = assemblyItem.GetMetadata ("Culture");
726+
if (!String.IsNullOrEmpty (culture)) {
727+
return $"{culture}/{name}";
728+
}
729+
return name;
730+
}
731+
684732
static Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> GetPerArchAssemblies (IEnumerable<ITaskItem> input, HashSet<AndroidTargetArch> supportedTargetArches, bool validate, Func<ITaskItem, bool>? shouldSkip = null)
685733
{
686734
bool filterByTargetArches = supportedTargetArches.Count > 0;
@@ -700,12 +748,7 @@ static Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> GetPerArchAs
700748
assembliesPerArch.Add (arch, assemblies);
701749
}
702750

703-
string name = Path.GetFileNameWithoutExtension (assembly.ItemSpec);
704-
string? culture = assembly.GetMetadata ("Culture");
705-
if (!String.IsNullOrEmpty (culture)) {
706-
name = $"{culture}/{name}";
707-
}
708-
assemblies.Add (name, assembly);
751+
assemblies.Add (GetAssemblyNameWithCulture (assembly), assembly);
709752
}
710753

711754
// It's possible some assembly collections will be empty (e.g. `ResolvedUserAssemblies` as passed to the `GenerateJavaStubs` task), which

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class NativeAotDsoLoadNativeAssemblyGenerator : LlvmIrComposer
1212
readonly List<string>? runtimeComponentsJniOnLoadHandlers;
1313
readonly List<string>? customJniOnLoadHandlers;
1414

15-
public NativeAotDsoLoadNativeAssemblyGenerator (List<string>? runtimeComponentsJniOnLoadHandlers, List<string>? customJniOnLoadHandlers, TaskLoggingHelper log)
15+
public NativeAotDsoLoadNativeAssemblyGenerator (TaskLoggingHelper log, List<string>? runtimeComponentsJniOnLoadHandlers, List<string>? customJniOnLoadHandlers)
1616
: base (log)
1717
{
1818
this.runtimeComponentsJniOnLoadHandlers = runtimeComponentsJniOnLoadHandlers;

0 commit comments

Comments
 (0)