diff --git a/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/ParseAndroidResources.cs b/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/ParseAndroidResources.cs new file mode 100644 index 000000000..5b69a69c6 --- /dev/null +++ b/build-tools/Java.Interop.BootstrapTasks/Java.Interop.BootstrapTasks/ParseAndroidResources.cs @@ -0,0 +1,66 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System; +using System.Linq; +using System.IO; +using System.Collections.Generic; + + +namespace Java.Interop.BootstrapTasks +{ + public class ParseAndroidResources : Task + { + public ITaskItem AndroidResourceFile { get; set; } + public ITaskItem OutputFile { get; set; } + public string DeclaringNamespaceName { get; set; } + public string DeclaringClassName { get; set; } + + public override bool Execute () + { + using (var o = File.CreateText (OutputFile.ItemSpec)) { + o.WriteLine ($"namespace {DeclaringNamespaceName};"); + o.WriteLine (); + o.WriteLine ($"partial class {DeclaringClassName} {{"); + var resources = ParseAndroidResourceFile (AndroidResourceFile.ItemSpec); + foreach (var declType in resources.Keys.OrderBy (x => x)) { + o.WriteLine ($"\tpublic static class @{declType} {{"); + var decls = resources [declType]; + foreach (var decl in decls.Keys.OrderBy (x => x)) { + o.WriteLine ($"\t\tpublic const int {decl} = {decls [decl]};"); + } + o.WriteLine ("\t}"); + } + o.WriteLine ("}"); + o.WriteLine (); + } + + return !Log.HasLoggedErrors; + } + + Dictionary> ParseAndroidResourceFile (string file) + { + var resources = new Dictionary> (); + using (var reader = File.OpenText (file)) { + string line; + while ((line = reader.ReadLine ()) != null) { + if (line.StartsWith ("#")) + continue; + var items = line.Split (' '); + if (items.Length != 4) + continue; + var type = items [0]; + if (string.Compare (type, "int", StringComparison.Ordinal) != 0) + continue; + var decl = items [1]; + var name = items [2]; + var value = items [3]; + if (!resources.TryGetValue (decl, out var declResources)) + resources.Add (decl, declResources = new Dictionary()); + declResources.Add (name, value); + } + } + + return resources; + } + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/.gitignore b/samples/Hello-NativeAOTFromAndroid/.gitignore new file mode 100644 index 000000000..10b74d7f3 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/.gitignore @@ -0,0 +1,2 @@ +.gradle +android.xml.fixed diff --git a/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.csproj b/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.csproj new file mode 100644 index 000000000..77ce881da --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.csproj @@ -0,0 +1,49 @@ + + + + $(DotNetTargetFramework) + + + + + + Java.Interop.Samples.NativeAotFromAndroid + enable + enable + true + true + true + Shared + linux-bionic-arm64 + + AnyCPU + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.targets b/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.targets new file mode 100644 index 000000000..c9f836e93 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/Hello-NativeAOTFromAndroid.targets @@ -0,0 +1,221 @@ + + + + + + $(UtilityOutputFullPath)generator.dll + <_JcwOutputDir>app/src/main/java/my/ + <_GradleJniLibsDir>app/src/main/jniLibs/arm64-v8a + $(ANDROID_NDK_HOME) + $(ANDROID_HOME) + + + + <_NdkSysrootAbi Condition=" '$(RuntimeIdentifier)' == 'linux-bionic-arm64' ">aarch64-linux-android + <_NdkClangPrefix Condition=" '$(RuntimeIdentifier)' == 'linux-bionic-arm64' ">aarch64-linux-android21- + <_NdkSysrootAbi Condition=" '$(RuntimeIdentifier)' == 'linux-bionic-x64' ">x86_64-linux-android + <_NdkClangPrefix Condition=" '$(RuntimeIdentifier)' == 'linux-bionic-x64' ">x86_64-linux-android21- + <_NdkPrebuiltAbi Condition=" '$(NETCoreSdkRuntimeIdentifier)' == 'osx-x64' ">darwin-x86_64 + <_NdkSysrootLibDir>$(AndroidNdkDirectory)/toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/sysroot/usr/lib/$(_NdkSysrootAbi) + <_NdkBinDir>$(AndroidNdkDirectory)/toolchains/llvm/prebuilt/$(_NdkPrebuiltAbi)/bin + + + + $(_NdkBinDir)/$(_NdkClangPrefix)clang + $(_NdkBinDir)/llvm-objcopy + + + + + + + + + + + + + + <_GenerateAndroidBindingInputs Include="$(GeneratorPath)" /> + <_GenerateAndroidBindingInputs Include="$(MSBuildThisFileFullPath)" /> + <_GenerateAndroidBindingInputs Include="Transforms\**" /> + <_GenerateAndroidBindingInputs Include="$(IntermediateOutputPath)mcw\api.xml" /> + + + + + + "$(GeneratorPath)" + <_GenFlags>--public --global + <_Out>-o "$(IntermediateOutputPath)mcw" + <_Codegen>--codegen-target=JavaInterop1 + <_Fixup>--fixup=Transforms/Metadata.xml + <_Enums1>--preserve-enums --enumflags=Transforms/enumflags --enumfields=Transforms/map.csv --enummethods=Transforms/methodmap.csv + <_Enums2>--enummetadata=$(IntermediateOutputPath)mcw/enummetadata + <_Assembly>"--assembly=$(AssemblyName)" + <_TypeMap>--type-map-report=$(IntermediateOutputPath)mcw/type-mapping.txt + <_Api>android.xml + <_Dirs>--enumdir=$(IntermediateOutputPath)mcw + <_FullIntermediateOutputPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)')) + <_LangFeatures>--lang-features=nullable-reference-types,default-interface-methods,nested-interface-types,interface-constants + + + <_RefAsmDir Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + <_Lib Include="@(_RefAsmDir->'-L "%(Identity)"')" /> + <_JavaBaseRef Include="@(ReferencePathWithRefAssemblies)" + Condition=" '%(FileName)' == 'Java.Base' " + /> + <_Ref Include="@(_JavaBaseRef->'-r "%(FullPath)"')" /> + + + + + + + + + + + + + $(DefineConstants);$([System.String]::Copy('$(_GeneratedDefineConstants)').Replace ('%24(DefineConstants);', '')) + + + + + + + + + <_JcwGenRefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + + + <_JcwGen>"$(UtilityOutputFullPath)/jcw-gen.dll" + <_Target>--codegen-target JavaInterop1 + <_Output>-o "$(_JcwOutputDir)" + <_Libpath>@(_JcwGenRefAsmDirs->'-L "%(Identity)"', ' ') + + + + + + + + + <_JnimmRefAsmDirs Include="@(RuntimePackAsset->'%(RootDir)%(Directory).'->Distinct())" /> + + + <_JnimarshalmethodGen>"$(UtilityOutputFullPath)/jnimarshalmethod-gen.dll" + <_Verbosity>-v -v --keeptemp + <_Libpath>-L "$(TargetDir)" @(_JnimmRefAsmDirs->'-L "%(Identity)"', ' ') + + + + + + + + + + + + + <_BuildAppApkInput Include="$(MSBuildThisFileFullPath)" /> + <_BuildAppApkInput Include="app\src\main\java\**\*.java" /> + <_BuildAppApkInput Include="app\src\main\AndroidManifest.xml" /> + <_BuildAppApkInput Include="app\**\build.gradle" /> + <_BuildAppApkInput Include="$(NativeBinary)" /> + <_BuildAppApkInput Include="$(OutputPath)java-interop.jar" /> + + + + <_AfterBuildDependsOnTargets> + _CreateJavaCallableWrappers; + _AddMarshalMethods; + + + + + + + <_GradleRtxtPath>app\build\intermediates\runtime_symbol_list\release\R.txt + + + + + + + + + + + + + + + <_GradleBuildSource Include="$(NativeBinary)" /> + <_GradleBuildTarget Include="$(_GradleJniLibsDir)\lib$(AssemblyName)$(NativeBinaryExt)" /> + + <_GradleBuildSource Include="$(OutputPath)java-interop.jar" /> + <_GradleBuildTarget Include="app\lib\java-interop.jar" /> + + + + + + diff --git a/samples/Hello-NativeAOTFromAndroid/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromAndroid/JavaInteropRuntime.cs new file mode 100644 index 000000000..74d7e21f8 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/JavaInteropRuntime.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +using Java.Interop; + +namespace Java.Interop.Samples.NativeAotFromAndroid; + +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) + { + Console.WriteLine ($"C# init()"); + try { + var options = new JreRuntimeOptions { + EnvironmentPointer = jnienv, + TypeManager = new NativeAotTypeManager (), + UseMarshalMemberBuilder = false, + JniGlobalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:GREF"), + JniLocalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:LREF"), + }; + runtime = options.CreateJreVM (); + } + catch (Exception e) { + AndroidLog.Print (AndroidLogLevel.Error, "JavaInteropRuntime", $"JavaInteropRuntime.init: error: {e}"); + } + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/LogcatTextWriter.cs b/samples/Hello-NativeAOTFromAndroid/LogcatTextWriter.cs new file mode 100644 index 000000000..36f2f2a90 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/LogcatTextWriter.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Java.Interop.Samples.NativeAotFromAndroid; + +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 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 +} diff --git a/samples/Hello-NativeAOTFromAndroid/MainActivity.cs b/samples/Hello-NativeAOTFromAndroid/MainActivity.cs new file mode 100644 index 000000000..6eefab692 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/MainActivity.cs @@ -0,0 +1,39 @@ +using Java.Interop; + +namespace Java.Interop.Samples.NativeAotFromAndroid; + +[JniTypeSignature ("my/MainActivity")] +public class MainActivity : Android.App.Activity { + + public MainActivity () + { + Console.WriteLine ("MainActivity..ctor()"); + } + + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) + { + Console.WriteLine ($"MainActivity.OnCreate(): savedInstanceState? {savedInstanceState != null}"); + base.OnCreate (savedInstanceState); + SetContentView (R.layout.activity_main); + + PrintGrefInfo (); + } + + static void PrintGrefInfo () + { + var runtime = JniEnvironment.Runtime; + var peers = runtime.ValueManager.GetSurfacedPeers (); + Console.WriteLine ($"Created {runtime.ObjectReferenceManager.GlobalReferenceCount} GREFs; Surfaced {peers.Count} peers"); + for (int i = 0; i < peers.Count; ++i) { + Console.WriteLine ($" SurfacedPeers[{i,3}] = {ToString (peers[i])}"); + } + } + + static string ToString (JniSurfacedPeerInfo peer) + { + if (!peer.SurfacedPeer.TryGetTarget (out var p)) { + return $"JniSurfacedPeerInfo(IdentityHashCode=0x{peer.JniIdentityHashCode:x})"; + } + return $"JniSurfacedPeerInfo(PeerReference={p.PeerReference} IdentityHashCode=0x{peer.JniIdentityHashCode:x} Instance.Type={p.GetType ()})"; + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs new file mode 100644 index 000000000..22f5bd7f9 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; + +using Java.Interop; + +namespace Java.Interop.Samples.NativeAotFromAndroid; + +partial class NativeAotTypeManager : JniRuntime.JniTypeManager { + + internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + + Dictionary typeMappings = new () { + ["android/app/Activity"] = typeof (Android.App.Activity), + ["android/content/Context"] = typeof (Android.Content.Context), + ["android/content/ContextWrapper"] = typeof (Android.Content.ContextWrapper), + ["android/os/BaseBundle"] = typeof (Android.OS.BaseBundle), + ["android/os/Bundle"] = typeof (Android.OS.Bundle), + ["android/view/ContextThemeWrapper"] = typeof (Android.View.ContextThemeWrapper), + ["my/MainActivity"] = typeof (MainActivity), + }; + + public override void RegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + ReadOnlySpan methods) + { + Console.WriteLine ($"# jonp: RegisterNativeMembers: nativeClass={nativeClass} type=`{type}`"); + base.RegisterNativeMembers (nativeClass, type, methods); + } + + + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) + { + Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}`"); + if (typeMappings.TryGetValue (jniSimpleReference, out var target)) { + Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}` -> `{target}`"); + yield return target; + } + foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) { + Console.WriteLine ($"# jonp: GetTypesForSimpleReference: jniSimpleReference=`{jniSimpleReference}` -> `{t}`"); + yield return t; + } + } + + protected override IEnumerable GetSimpleReferences (Type type) + { + return base.GetSimpleReferences (type) + .Concat (CreateSimpleReferencesEnumerator (type)); + } + + IEnumerable CreateSimpleReferencesEnumerator (Type type) + { + if (typeMappings == null) + yield break; + foreach (var e in typeMappings) { + if (e.Value == type) + yield return e.Key; + } + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/README.md b/samples/Hello-NativeAOTFromAndroid/README.md new file mode 100644 index 000000000..628121a7a --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/README.md @@ -0,0 +1,224 @@ +# Hello From Android + +[Hello-NativeAOTFromJNI](../Hello-NativeAOTFromJNI) demonstrated how +to use [NativeAOT][0] to create a native library which could be loaded +by a Java Virtual Machine (JVM). + +Extend this idea for Android! + +## Building + +Building a native library with NativeAOT requires a Release configuration build. +For in-repo use, that means that xamarin/Java.Interop itself needs to be built in +Release configuration: + +```sh +% dotnet build -c Release -t:Prepare +% dotnet build -c Release +``` + +Once Java.Interop itself is built, you can *publish* the sample: + +```sh +% cd samples/Hello-NativeAOTFromAndroid + +# set the ANDROID_NDK_HOME environment variable or set the AndroidNdkDirectory property +# set the ANDROID_HOME environment variable or set the AndroidSdkDirectory property +# values here are valid if you have a xamarin/xamarin-android build environment. +% dotnet publish -c Release -p:AndroidNdkDirectory=$HOME/android-toolchain/ndk \ + -p:AndroidSdkDirectory=$HOME/android-toolchain/sdk +``` + +The resulting native library contains various import symbols: + +```sh +% nm -D bin/Release/linux-bionic-arm64/native/Hello-NativeAOTFromAndroid.so | grep ' T ' +0000000000240950 T JNI_OnLoad@@V1.0 +0000000000240ab0 T JNI_OnUnload@@V1.0 +0000000000240b30 T Java_net_dot_jni_nativeaot_JavaInteropRuntime_init@@V1.0 +00000000002392e0 T __start___managedcode +00000000004394d0 T __start___unbox +00000000004394d0 T __stop___managedcode +000000000043a720 T __stop___unbox +``` + +The build system also produces a `net.dot.jni.helloandroid-Signed.apk`, +which can be installed and launched: + +```sh +% adb install bin/Release/linux-bionic-arm64/net.dot.jni.helloandroid-Signed.apk +% adb shell am start net.dot.jni.helloandroid/my.MainActivity + +# Only-java codepath for testing; doesn't use NativeAOT: +% adb shell am start net.dot.jni.helloandroid/net.dot.jni.nativeaot.JavaMainActivity +``` + +## Logging + +By default this sample writes quite a bit to `adb logcat`, including: + + * Initialization messages + + ``` + D NativeAotRuntimeProvider: NativeAotRuntimeProvider() + D NativeAotRuntimeProvider: NativeAotRuntimeProvider.attachInfo(): calling JavaInteropRuntime.init()… + D JavaInteropRuntime: Loading libHello-NativeAOTFromAndroid.so… + I JavaInteropRuntime: JNI_OnLoad() + I NativeAotFromAndroid: C# init() + D NativeAotRuntimeProvider: NativeAotRuntimeProvider.onCreate() + ``` + + * JNI Global Reference and Local Reference messages + + ``` + D NativeAot:LREF: +l+ lrefc 1 handle 0x7eb64ae01d/L from thread ''(1) + D NativeAot:GREF: +g+ grefc 1 obj-handle 0x7eb64ae01d/L -> new-handle 0x2af2/G from thread ''(1) + ``` + + * `MainActivity` messages + + ``` + I NativeAotFromAndroid: MainActivity..ctor() + I NativeAotFromAndroid: MainActivity.OnCreate(): savedInstanceState? False + ``` + +Additionally, the end of `MainActivity.OnCreate()` will print out how many +GREFs have been created, and information about the created "surfaced peers": + +``` +I NativeAotFromAndroid: Created 6 GREFs; Surfaced 1 peers +I NativeAotFromAndroid: SurfacedPeers[ 0] = JniSurfacedPeerInfo(PeerReference=0x2bc6/G IdentityHashCode=0x1d64f40 Instance.Type=Java.Interop.Samples.NativeAotFromAndroid.MainActivity) +``` + +The (very!) extensive logging around JNI Global and Local references mean that +this sample should *not* be used as-is for startup timing comparison. +That said, on my Pixel 6, we get: + +``` +I ActivityTaskManager: Displayed net.dot.jni.helloandroid/my.MainActivity for user 0: +282ms +``` + +## What does this mean for .NET for Android? + +Short-term? Nothing. Long-term? *Maybe* something. + +While .NET for Android uses Java.Interop, it uses a different *style* of Java.Interop. +.NET for Android *could* be updated to support NativeAot, but it would not be as simple +as this sample may suggest. Difficulties will include: + + * [GC](#gc) + * [Marshal Methods](#marshal-methods) + * [Process Startup miscellany, including the important question "what is an Assembly?"](#miscellany) + +### GC + +.NET for Android relies on .NET's MonoVM, which provides a +[GC bridge](https://github.com/dotnet/runtime/blob/c5c7f0d3d11cc82eddf1747fbdcaec9cb850c3aa/src/native/public/mono/metadata/details/sgen-bridge-types.h), +which is used to support cross-VM object references. This allows an object +reference within a Java VM to keep an object instance within the .NET VM alive. + +Neither CoreCLR nor NativeAot runtimes support such a GC bridge, and without +something like it, developers would need to take *significantly* more care in +object lifetimes and cleanup. + +Until a cross-VM GC solution is found, .NET for Android must remain on MonoVM. + +### Marshal Methods + +"Marshal Methods" are methods that are: + + * Invoked by the Java Virtual Machine when a `native` Java method is invoked. + * Responsible for parameter marshaling, invoking C# method overrides, and +marshaling the return type back to Java. + +.NET for Android uses `generator --codegen-target=XAJavaInterop1` for binding +assemblies, which "bakes in" marshal methods. There is an implicit ABI for +marshal methods, and part of that ABI is that they don't catch exceptions: + +```csharp +partial class Activity { + protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) => … + + static Delegate? cb_onCreate_Landroid_os_Bundle_; + static Delegate GetOnCreate_Landroid_os_Bundle_Handler () + { + if (cb_onCreate_Landroid_os_Bundle_ == null) + cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate (new _JniMarshal_PPL_V (n_OnCreate_Landroid_os_Bundle_)); + return cb_onCreate_Landroid_os_Bundle_; + } + + static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + { + // Note: no try/catch block! If `__this.OnCreate()` throws, Bad Things™ will happen. + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); + __this.OnCreate (savedInstanceState); + } +} +``` + +`Activity.n_OnCreate_Landroid_os_Bundle_()` is the marshal method responsible for +invoking `Activity.OnCreate()`. It does not catch exceptions, and if an exception +*were* thrown from `Activity.OnCreate()`, the entire app could exit. Consequently, +every such marshal method is wrapped in `JNINativeWrapper.CreateDelegate()`, which +uses `DynamicMethod` to wrap the marshal method in a `try`/`catch` block, which +is responsible for notifying the debugger and exception marshaling. + +As-is, none of this can work with NativeAot. + +Updating .NET for Android to *not* use `DynamicMethod` has both known and unknown +issues (what new pattern do we use? What about compatibility with existing +binding assemblies?). + +This sample uses `generator --codegen-target=JavaInterop1` for binding assemblies, +which *skips* the emission of marshal methods *entirely*. As Marshal Methods are +*required*, `jnimarshalmethod-gen` is invoked as a post-build step to insert +Marshal Methods into the assemblies, and these marshal methods appropriately +marshal exceptions. + +## Miscellany + +.NET for Android deals with assemblies: they can be side-loaded (for Fast Deployment), +packaged trimmed or untrimmed. Bidirectional mapping between JNI type names and +`System.Type` instances makes extensive use of MonoVM's embedding API. + +None of the above exists in NativeAot: there are no separate assembly files, +"assembly identity" is a nebulous concept, and there is no equivalent to the MonoVm +embedding API. + +Large portions of .NET for Android would need to be rewritten to support NativeAot, +and NativeAot would actively prevent features such as Fast Deployment, meaning *both* +MonoVM and NativeAot would need to be supported. + +## Notes + +As with `Hello-NativeAOTFromJNI`, the project needs to be built with +`$(PlatformTarget)`=AnyCPU, so that `jnimarshalmethod-gen` can be used +to generate JNI Marshal Methods as a post-build step. + +This project contains a *tiny* `android.xml` API description for Android. +This is used to generate a binding, allowing (nominally) intuitive: + +```csharp +[JniTypeSignature ("my/MainActivity")] +partial class MainActivity : Android.App.Activity { + protected override void OnCreate (Android.OS.Bundle? savedInstanceState) => … +} +``` + +This project follows what .NET for Android does to initialize things: +provide a custom [`ContentProvider`][1] which contains Java "bootstrap" +code to initialize the runtime. + +### GC + +As with [Hello-NativeAOTFromJNI](../Hello-NativeAOTFromJNI), NativeAOT does not +provide a GC bridge that we can rely on. Consequently, every "surfaced peer" will +*never be collected by default*. + +This is a *sample*, not a product, and not even the *inkling* of a product. + +For exploratory purposes only. + +[0]: https://github.com/dotnet/samples/blob/main/core/nativeaot/NativeLibrary/README.md +[1]: https://developer.android.com/reference/android/content/ContentProvider diff --git a/samples/Hello-NativeAOTFromAndroid/Transforms/Metadata.xml b/samples/Hello-NativeAOTFromAndroid/Transforms/Metadata.xml new file mode 100644 index 000000000..26c2f6238 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/Transforms/Metadata.xml @@ -0,0 +1,2 @@ + + diff --git a/samples/Hello-NativeAOTFromAndroid/Transforms/enumflags b/samples/Hello-NativeAOTFromAndroid/Transforms/enumflags new file mode 100644 index 000000000..e69de29bb diff --git a/samples/Hello-NativeAOTFromAndroid/Transforms/map.csv b/samples/Hello-NativeAOTFromAndroid/Transforms/map.csv new file mode 100644 index 000000000..e69de29bb diff --git a/samples/Hello-NativeAOTFromAndroid/Transforms/methodmap.csv b/samples/Hello-NativeAOTFromAndroid/Transforms/methodmap.csv new file mode 100644 index 000000000..e69de29bb diff --git a/samples/Hello-NativeAOTFromAndroid/android.xml b/samples/Hello-NativeAOTFromAndroid/android.xml new file mode 100644 index 000000000..d52bd14b3 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/android.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Hello-NativeAOTFromAndroid/app/.gitignore b/samples/Hello-NativeAOTFromAndroid/app/.gitignore new file mode 100644 index 000000000..13775520f --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/.gitignore @@ -0,0 +1,5 @@ +/build +lib/java-interop.jar +src/main/java/my +src/main/jniLibs + diff --git a/samples/Hello-NativeAOTFromAndroid/app/build.gradle b/samples/Hello-NativeAOTFromAndroid/app/build.gradle new file mode 100644 index 000000000..2ac125e3e --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.application' +} + +dependencies { + implementation files('lib/java-interop.jar') +} + +android { + namespace 'net.dot.jni.helloandroid' + compileSdk 33 + // Doing this to match NDK NativeAOT is using + ndkVersion "23.2.8568313" + + defaultConfig { + applicationId "net.dot.jni.helloandroid" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0" + + // NOTE: for now, arm64 only. Might eventually do 4 ABIs + ndk { + abiFilters 'arm64-v8a' + } + } + + // Just use the built-in debug keystore + signingConfigs { + release { + storeFile file("${System.getProperty('user.home')}/.android/debug.keystore") + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/app/proguard-rules.pro b/samples/Hello-NativeAOTFromAndroid/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/AndroidManifest.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6e1f849a7 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaInteropRuntime.java b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaInteropRuntime.java new file mode 100644 index 000000000..26811bed8 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaInteropRuntime.java @@ -0,0 +1,15 @@ +package net.dot.jni.nativeaot; + +import android.util.Log; + +public class JavaInteropRuntime { + static { + Log.d("JavaInteropRuntime", "Loading libHello-NativeAOTFromAndroid.so…"); + System.loadLibrary("Hello-NativeAOTFromAndroid"); + } + + private JavaInteropRuntime() { + } + + public static native void init(); +} diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaMainActivity.java b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaMainActivity.java new file mode 100644 index 000000000..b8334ac25 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/JavaMainActivity.java @@ -0,0 +1,18 @@ +package net.dot.jni.nativeaot; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; + +import net.dot.jni.helloandroid.R; + +public class JavaMainActivity extends Activity { + private static final String TAG = "NativeAot:JavaMainActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "JavaMainActivity.onCreate()"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/NativeAotRuntimeProvider.java b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/NativeAotRuntimeProvider.java new file mode 100644 index 000000000..9ac3259af --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/java/net/dot/jni/nativeaot/NativeAotRuntimeProvider.java @@ -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."); + } +} diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/libs/arm64-v8a/libdotnet.so.dbg b/samples/Hello-NativeAOTFromAndroid/app/src/main/libs/arm64-v8a/libdotnet.so.dbg new file mode 100644 index 000000000..dfb82ed6d Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/libs/arm64-v8a/libdotnet.so.dbg differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable/ic_launcher_background.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/layout/activity_main.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..dd60405e5 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/samples/Hello-NativeAOTFromAndroid/app/src/main/res/values/strings.xml b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..4378cbc73 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Hello NativeAOT from Android! + diff --git a/samples/Hello-NativeAOTFromAndroid/build.gradle b/samples/Hello-NativeAOTFromAndroid/build.gradle new file mode 100644 index 000000000..24469b8d3 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/build.gradle @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + // Need to use < 8.0 in order to use JDK-11 + id 'com.android.application' version '7.4.0' apply false +} diff --git a/samples/Hello-NativeAOTFromAndroid/gradle.properties b/samples/Hello-NativeAOTFromAndroid/gradle.properties new file mode 100644 index 000000000..a03b35489 --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/samples/Hello-NativeAOTFromAndroid/settings.gradle b/samples/Hello-NativeAOTFromAndroid/settings.gradle new file mode 100644 index 000000000..e901c9c1b --- /dev/null +++ b/samples/Hello-NativeAOTFromAndroid/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Hello-NativeAOTFromAndroid" +include ':app' diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.Adapters/CecilImporter.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.Adapters/CecilImporter.cs index b56fd814e..9353e8378 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.Adapters/CecilImporter.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.Adapters/CecilImporter.cs @@ -126,7 +126,7 @@ static CallableWrapperType CreateType (TypeDefinition type, IMetadataResolver re foreach (var bt in type.GetBaseTypes (resolver)) { ctorTypes.Add (bt); - var rattr = CecilExtensions.GetMethodRegistrationAttributes (bt).FirstOrDefault (); + var rattr = CecilExtensions.GetTypeRegistrationAttributes (bt).FirstOrDefault (); if (rattr != null && rattr.DoNotGenerateAcw) break; diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/CecilExtensions.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/CecilExtensions.cs index 038d407bb..74e10e08c 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/CecilExtensions.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/CecilExtensions.cs @@ -45,7 +45,7 @@ internal static ExportFieldAttribute ToExportFieldAttribute (CustomAttribute att while ((bmethod = method.GetBaseDefinition (cache)) != method) { method = bmethod; - if (method.AnyCustomAttributes (typeof (RegisterAttribute))) { + if (HasMethodRegistrationAttributes (method)) { return method; } } @@ -168,6 +168,7 @@ internal static IEnumerable GetTypeRegistrationAttributes (Mo } } + // Keep in sync w/ HasMethodRegistrationAttributes() public static IEnumerable GetMethodRegistrationAttributes (Mono.Cecil.ICustomAttributeProvider p) { foreach (var a in CecilExtensions.GetAttributes (p, a => CecilExtensions.ToRegisterAttribute (a))) { @@ -189,6 +190,25 @@ public static IEnumerable GetMethodRegistrationAttributes (Mo } } + static readonly string[] MethodRegistrationAttributes = new[]{ + typeof (RegisterAttribute).FullName, + "Java.Interop.JniConstructorSignatureAttribute", + "Java.Interop.JniMethodSignatureAttribute", + }; + + // Keep in sync w/ GetMethodRegistrationAttributes() + public static bool HasMethodRegistrationAttributes (Mono.Cecil.ICustomAttributeProvider p) + { + foreach (CustomAttribute custom_attribute in p.CustomAttributes) { + var customAttrType = custom_attribute.Constructor.DeclaringType.FullName; + foreach (var t in MethodRegistrationAttributes) { + if (customAttrType == t) + return true; + } + } + return false; + } + public static IEnumerable GetExportAttributes (IMemberDefinition p, IMetadataResolver cache) { return CecilExtensions.GetAttributes (p, a => CecilExtensions.ToExportAttribute (a, p, cache)) diff --git a/src/Java.Interop.Tools.JavaTypeSystem/Adapters/ManagedApiImporter.cs b/src/Java.Interop.Tools.JavaTypeSystem/Adapters/ManagedApiImporter.cs index 9326743e1..abf05d815 100644 --- a/src/Java.Interop.Tools.JavaTypeSystem/Adapters/ManagedApiImporter.cs +++ b/src/Java.Interop.Tools.JavaTypeSystem/Adapters/ManagedApiImporter.cs @@ -330,7 +330,11 @@ static string GetBaseTypeJni (TypeDefinition type) attributes.FirstOrDefault (a => a.AttributeType.FullNameCorrected () == "System.ObsoleteAttribute"); static CustomAttribute? GetRegisterAttribute (Collection attributes) => - attributes.FirstOrDefault (a => a.AttributeType.FullNameCorrected () == "Android.Runtime.RegisterAttribute"); + attributes.FirstOrDefault (a => { + var attrType = a.AttributeType.FullNameCorrected (); + return attrType == "Android.Runtime.RegisterAttribute" || + attrType == "Java.Interop.JniTypeSignatureAttribute"; + }); static string? GetRegisteredJavaTypeName (TypeDefinition type) { diff --git a/tools/generator/Java.Interop.Tools.Generator.Importers/CecilApiImporter.cs b/tools/generator/Java.Interop.Tools.Generator.Importers/CecilApiImporter.cs index d679cec90..ae9fc3fbf 100644 --- a/tools/generator/Java.Interop.Tools.Generator.Importers/CecilApiImporter.cs +++ b/tools/generator/Java.Interop.Tools.Generator.Importers/CecilApiImporter.cs @@ -250,7 +250,11 @@ static string GetObsoleteComment (CustomAttribute attribute) => attribute?.ConstructorArguments.Any () == true ? (string) attribute.ConstructorArguments [0].Value : null; static CustomAttribute GetRegisterAttribute (Collection attributes) => - attributes.FirstOrDefault (a => a.AttributeType.FullNameCorrected () == "Android.Runtime.RegisterAttribute"); + attributes.FirstOrDefault (a => { + var attrType = a.AttributeType.FullNameCorrected (); + return attrType == "Android.Runtime.RegisterAttribute" || + attrType == "Java.Interop.JniTypeSignatureAttribute"; + }); static bool IsDefaultInterfaceMethod (GenBase declaringType, MethodDefinition method) {