Skip to content

Commit

Permalink
[NativeAOT] add sample that runs successfully (#9636)
Browse files Browse the repository at this point in the history
Context: https://github.com/dotnet/java-interop/tree/9b1d8781e8e322849d05efac32119c913b21c192/samples/Hello-NativeAOTFromAndroid

"Import" the [Hello-NativeAOTFromAndroid][0] from
dotnet/java-interop, and update it to use `Mono.Android.dll` and
parts of the .NET for Android build system.

It currently relies on `[InternalsVisibleTo("NativeAOT")]` within
`Mono.Android.dll` for access to things like:

  * `Android.Runtime.JNIEnvInit.InitializeJniRuntime()`

There are a couple MSBuild changes still left:

TODO:

  * `$(_ExtraTrimmerArgs)` needs to be specified for trimmer warnings
    to not be displayed twice.  This is the same thing done in
    xamarin/xamarin-macios.

  * `@(TrimmerRootAssembly)` needs to be set for illink's
    "already trimmed" output for ILC.  We exclude
    `System.Private.CoreLib.dll` from this list.

  * Remove use of `Java.Runtime.Environment.dll`, and otherwise allow
    the sample to be built from the .NET for Android workload packs.

  * "MOAR Integration": sample depends upon a manually specified
    "type map" dictionary.  This needs to be automagic to be useful.

  * Figure out what to do about C++: do we dynamically link against
    and bundle `libc++_shared.so`?
    Statically link against `libc++_static.a`?

[0]: dotnet/java-interop@78d5937
  • Loading branch information
jonathanpeppers authored Jan 22, 2025
1 parent 80bb5d7 commit 7a772f0
Show file tree
Hide file tree
Showing 34 changed files with 521 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ stages:
artifactName: $(TestAssembliesArtifactName)
downloadPath: ${{ parameters.xaSourcePath }}/bin/Test$(XA.Build.Configuration)

# Currently needed for samples/NativeAOT
- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self
parameters:
project: Xamarin.Android.sln
arguments: -t:PrepareJavaInterop -c $(XA.Build.Configuration) --no-restore
displayName: prepare java.interop $(XA.Build.Configuration)
continueOnError: false

- template: /build-tools/automation/yaml-templates/start-stop-emulator.yaml
parameters:
xaSourcePath: ${{ parameters.xaSourcePath }}
Expand Down
13 changes: 13 additions & 0 deletions samples/NativeAOT/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:label="@string/app_name" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
<!-- Temporary, to eventually be included in .NET Android infrastructure -->
<provider
android:name="net.dot.jni.nativeaot.NativeAotRuntimeProvider"
android:exported="false"
android:initOrder="1999999999"
android:authorities="net.dot.jni.nativeaot.NativeAotRuntimeProvider.__init__"
/>
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
54 changes: 54 additions & 0 deletions samples/NativeAOT/JavaInteropRuntime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Android.Runtime;
using Java.Interop;
using System.Runtime.InteropServices;

namespace NativeAOT;

static class JavaInteropRuntime
{
static JniRuntime? runtime;

[UnmanagedCallersOnly (EntryPoint="JNI_OnLoad")]
static int JNI_OnLoad (IntPtr vm, IntPtr reserved)
{
try {
AndroidLog.Print (AndroidLogLevel.Info, "JavaInteropRuntime", "JNI_OnLoad()");
LogcatTextWriter.Init ();
return (int) JniVersion.v1_6;
}
catch (Exception e) {
AndroidLog.Print (AndroidLogLevel.Error, "JavaInteropRuntime", $"JNI_OnLoad() failed: {e}");
return 0;
}
}

[UnmanagedCallersOnly (EntryPoint="JNI_OnUnload")]
static void JNI_OnUnload (IntPtr vm, IntPtr reserved)
{
AndroidLog.Print(AndroidLogLevel.Info, "JavaInteropRuntime", "JNI_OnUnload");
runtime?.Dispose ();
}

// symbol name from `$(IntermediateOutputPath)obj/Release/osx-arm64/h-classes/net_dot_jni_hello_JavaInteropRuntime.h`
[UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_nativeaot_JavaInteropRuntime_init")]
static void init (IntPtr jnienv, IntPtr klass)
{
try {
var options = new JreRuntimeOptions {
EnvironmentPointer = jnienv,
TypeManager = new NativeAotTypeManager (),
ValueManager = new NativeAotValueManager (),
UseMarshalMemberBuilder = false,
JniGlobalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:GREF"),
JniLocalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:LREF"),
};
runtime = options.CreateJreVM ();

// Entry point into Mono.Android.dll
JNIEnvInit.InitializeJniRuntime (runtime);
}
catch (Exception e) {
AndroidLog.Print (AndroidLogLevel.Error, "JavaInteropRuntime", $"JavaInteropRuntime.init: error: {e}");
}
}
}
15 changes: 15 additions & 0 deletions samples/NativeAOT/JavaInteropRuntime.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.dot.jni.nativeaot;

import android.util.Log;

public class JavaInteropRuntime {
static {
Log.d("JavaInteropRuntime", "Loading NativeAOT.so...");
System.loadLibrary("NativeAOT");
}

private JavaInteropRuntime() {
}

public static native void init();
}
77 changes: 77 additions & 0 deletions samples/NativeAOT/Logging.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// NOTE: logging methods below are need temporarily due to:
// 1) linux-bionic BCL doesn't redirect stdout/stderr to logcat
// 2) Android.Util.Log won't work until we initialize the Java.Interop.JreRuntime

using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace NativeAOT;

internal sealed class LogcatTextWriter : TextWriter {

public static void Init ()
{
// This method is a no-op, but it's necessary to ensure the static
// constructor is executed.
}

static LogcatTextWriter ()
{
Console.SetOut (new LogcatTextWriter (AndroidLogLevel.Info));
Console.SetError (new LogcatTextWriter (AndroidLogLevel.Error));
}

AndroidLogLevel Level;
string Tag;

internal LogcatTextWriter (AndroidLogLevel level, string tag = "NativeAotFromAndroid")
{
Level = level;
Tag = tag;
}

public override Encoding Encoding => Encoding.UTF8;
public override string NewLine => "\n";

public override void WriteLine (string? value)
{
if (value == null) {
AndroidLog.Print (Level, Tag, "");
return;
}
ReadOnlySpan<char> span = value;
while (!span.IsEmpty) {
if (span.IndexOf ('\n') is int n && n < 0) {
break;
}
var line = span.Slice (0, n);
AndroidLog.Print (Level, Tag, line.ToString ());
span = span.Slice (n + 1);
}
AndroidLog.Print (Level, Tag, span.ToString ());
}
}

static class AndroidLog {

[DllImport ("log", EntryPoint = "__android_log_print", CallingConvention = CallingConvention.Cdecl)]
private static extern void __android_log_print(AndroidLogLevel level, string? tag, string format, string args, IntPtr ptr);

internal static void Print(AndroidLogLevel level, string? tag, string message) =>
__android_log_print(level, tag, "%s", message, IntPtr.Zero);

}

internal enum AndroidLogLevel
{
Unknown = 0x00,
Default = 0x01,
Verbose = 0x02,
Debug = 0x03,
Info = 0x04,
Warn = 0x05,
Error = 0x06,
Fatal = 0x07,
Silent = 0x08
}
18 changes: 18 additions & 0 deletions samples/NativeAOT/MainActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Android.Runtime;
using System.Reflection;
using System.Runtime.InteropServices;

namespace NativeAOT;

[Register("my/MainActivity")] // Required for typemap in NativeAotTypeManager
[Activity(Label = "@string/app_name", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);

// Set our view from the "main" layout resource
SetContentView(Resource.Layout.activity_main);
}
}
36 changes: 36 additions & 0 deletions samples/NativeAOT/NativeAOT.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DotNetAndroidTargetFramework)</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationId>net.dot.hellonativeaot</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Temporary for InternalsVisibleTo -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
<!-- Default to arm64 device -->
<RuntimeIdentifier>android-arm64</RuntimeIdentifier>
<!-- Current required properties for NativeAOT -->
<PublishAot>true</PublishAot>
<PublishAotUsingRuntimePack>true</PublishAotUsingRuntimePack>
</PropertyGroup>

<!-- Settings for CI -->
<PropertyGroup Condition=" '$(RunningOnCI)' == 'true' ">
<!-- x86_64 emulator -->
<RuntimeIdentifier>android-x64</RuntimeIdentifier>
<_NuGetFolderOnCI>..\..\bin\Build$(Configuration)\nuget-unsigned</_NuGetFolderOnCI>
<RestoreAdditionalProjectSources Condition="Exists('$(_NuGetFolderOnCI)')">$(_NuGetFolderOnCI)</RestoreAdditionalProjectSources>
<_FastDeploymentDiagnosticLogging>true</_FastDeploymentDiagnosticLogging>
</PropertyGroup>

<ItemGroup>
<AndroidJavaSource Update="*.java" Bind="false" />
<ProjectReference Include="..\..\external\Java.Interop\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj" />
</ItemGroup>
</Project>
51 changes: 51 additions & 0 deletions samples/NativeAOT/NativeAotRuntimeProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package net.dot.jni.nativeaot;

import android.util.Log;

public class NativeAotRuntimeProvider
extends android.content.ContentProvider
{
private static final String TAG = "NativeAotRuntimeProvider";

public NativeAotRuntimeProvider() {
Log.d(TAG, "NativeAotRuntimeProvider()");
}

@Override
public boolean onCreate() {
Log.d(TAG, "NativeAotRuntimeProvider.onCreate()");
return true;
}

@Override
public void attachInfo(android.content.Context context, android.content.pm.ProviderInfo info) {
Log.d(TAG, "NativeAotRuntimeProvider.attachInfo(): calling JavaInteropRuntime.init()…");
JavaInteropRuntime.init();
super.attachInfo (context, info);
}

@Override
public android.database.Cursor query(android.net.Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
throw new RuntimeException ("This operation is not supported.");
}

@Override
public String getType(android.net.Uri uri) {
throw new RuntimeException ("This operation is not supported.");
}

@Override
public android.net.Uri insert(android.net.Uri uri, android.content.ContentValues initialValues) {
throw new RuntimeException ("This operation is not supported.");
}

@Override
public int delete(android.net.Uri uri, String where, String[] whereArgs) {
throw new RuntimeException ("This operation is not supported.");
}

@Override
public int update(android.net.Uri uri, android.content.ContentValues values, String where, String[] whereArgs) {
throw new RuntimeException ("This operation is not supported.");
}
}
Loading

0 comments on commit 7a772f0

Please sign in to comment.