Skip to content

Commit 69c90ba

Browse files
committed
"Full(er)" sample
What `Hello-NativeAOTFromJNI` previously did was quite minimal: 1. Use `[UnmanagedCallersOnly]` to provide a `JNI_OnLoad()` method which 2. Initialized the Java.Interop runtime, allowing 3. An `[UnmanagedCallersOnly]`-provided `Java_…` method which is called from Java. All quite low level, all miles away from .NET Android. Expand the sample to: 1. Contain a `Java.Lang.Object` subclass, which contains a `[JavaCallable]` method. 2. Call `jcw-gen` to generate Java Callable Wrappers for (1), containing the `[JavaCallable]` method. 3. Call `jnimarshalmethod-gen` to generate marshal methods for (1), as NativeAOT doesn't support System.Reflection.Emit. 4. Instantiate (1) *from Java*, and invoke the `[JavaCallable]` method. *Now* we're (kinda) getting something that looks like .NET Android. But first, we need to make that *work*: Update `Java.Interop.Tools.JavaCallableWrappers` so that it will emit `native` method declarations for `[JavaCallable]` methods, not just method overrides and `[Export]` methods. Update `Java.Interop.Tools.Expressions` so that the `_JniMarshal_*` delegate types have `[UnmanagedFunctionPointer(CallingConvention.Winapi)]`, as this is what allows NativeAOT to emit appropriate "stubs"; see also da9f188. Update `Java.Interop.Tools.Expressions.ExpressionAssemblyBuilder` to no longer attempt to "remove and fixup" `System.Private.CoreLib`. So long as `ExpressionAssemblyBuilder` output is *only* used in "completed" apps (not distributed in NuGet packages in some "intermediate" form), referencing `System.Private.CoreLib` is "fine". Additionally, trying to remove `System.Private.CoreLib` broke things when adding `[UnmanagedFunctionPointer]`, as `CallingConvention` could not be resolved, resulting in `jnimarshalmethod-gen` erroring out with: error JM4006: jnimarshalmethod-gen: Unable to process assembly '/Volumes/Xamarin-Work/src/xamarin/Java.Interop/samples/Hello-NativeAOTFromJNI/bin/Debug/Hello-NativeAOTFromJNI.dll' Failed to resolve System.Runtime.InteropServices.CallingConvention Mono.Cecil.ResolutionException: Failed to resolve System.Runtime.InteropServices.CallingConvention at Mono.Cecil.Mixin.CheckedResolve(TypeReference self) at Mono.Cecil.SignatureWriter.WriteCustomAttributeEnumValue(TypeReference enum_type, Object value) … (This is because `CallingConvention` is in `System.Runtime.InteropServices.dll`, which isn't referenced.) We could "fix" this by explicitly adding a reference to `System.Runtime.InteropServices.dll`, but this is just one of an unknown number of corner cases. Give up for now. Update `jnimarshalmethod-gen` assembly location probing: it was given the *full assembly name* of `Java.Base`: # jonp: resolving assembly: Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null …and failing to find `Java.Base.dll`, because it was looking for `Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null.dll`. Oops. Use `AssemblyName` to parse the string and extract out the assembly name., so that `Java.Base.dll` is probed for and found. With all that…it still fails: % (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App) Hello from Java! C# init() Hello from .NET NativeAOT! String returned to Java: Hello from .NET NativeAOT! Exception in thread "main" com.xamarin.java_interop.internal.JavaProxyThrowable: System.IO.FileNotFoundException: Could not resolve assembly 'Hello-NativeAOTFromJNI'. at System.Reflection.TypeNameParser.ResolveAssembly(String) + 0x97 at System.Reflection.TypeNameParser.GetType(String, ReadOnlySpan`1, String) + 0x32 at System.Reflection.TypeNameParser.NamespaceTypeName.ResolveType(TypeNameParser&, String) + 0x17 at System.Reflection.TypeNameParser.GetType(String, Func`2, Func`4, Boolean, Boolean, Boolean, String) + 0x99 at Java.Interop.ManagedPeer.RegisterNativeMembers(IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, IntPtr n_assemblyQualifiedName, IntPtr n_methods) + 0x103 at com.xamarin.java_interop.ManagedPeer.registerNativeMembers(Native Method) at example.ManagedType.<clinit>(ManagedType.java:15) at com.microsoft.hello_from_jni.App.main(App.java:13) `App.main()` has `new example.ManagedType()`, which hits the `ManagedType` static constructor of: public /* partial */ class ManagedType extends java.lang.Object implements com.xamarin.java_interop.GCUserPeerable { /** @hide */ public static final String __md_methods; static { __md_methods = "n_GetString:()Ljava/lang/String;:__export__\n" + ""; com.xamarin.java_interop.ManagedPeer.registerNativeMembers (ManagedType.class, "Example.ManagedType, Hello-NativeAOTFromJNI", __md_methods); } } The `ManagedPeer.registerNativeMembers()` call is what is needed to register the native `ManagedPeer.getString()` method, so that it can be called. This is good. (Though `__md_methods` containing *anything* is not desired, but that's a different problem.) `ManagedPeer.RegisterNativeMembers()` is given the assembly-qualified name `Example.ManagedType, Hello-NativeAOTFromJNI`, and tries to: Type.GetType ("Example.ManagedType, Hello-NativeAOTFromJNI", throwOnError: true); …which then proceeds to throw, because in NativeAOT *there are no assemblies*, and thus `Type.GetType()` *cannot work*. Oops. Thus, the only way to make something remotely like .NET Android infrastructure work is to *require* the use of `Java_…` native method names and `[UnmanagedCallersOnly]` on marshal methods. (In .NET Android parlance, the experimental `$(AndroidEnableMarshalMethods)`=True is required.)
1 parent da9f188 commit 69c90ba

File tree

11 files changed

+155
-26
lines changed

11 files changed

+155
-26
lines changed

samples/Hello-NativeAOTFromJNI/App.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Runtime.InteropServices;
2+
3+
using Java.Interop;
4+
5+
namespace Hello_NativeAOTFromJNI;
6+
7+
static class App {
8+
9+
// symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h`
10+
[UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_App_sayHello")]
11+
static IntPtr sayHello (IntPtr jnienv, IntPtr klass)
12+
{
13+
var s = $"Hello from .NET NativeAOT!";
14+
Console.WriteLine (s);
15+
var h = JniEnvironment.Strings.NewString (s);
16+
var r = JniEnvironment.References.NewReturnToJniRef (h);
17+
JniObjectReference.Dispose (ref h);
18+
return r;
19+
}
20+
}

samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<ProjectReference Include="..\..\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj"
1818
AdditionalProperties="Standalone=True"
1919
/>
20+
<ProjectReference Include="..\..\src\Java.Base\Java.Base.csproj" />
21+
<ProjectReference Include="..\..\src\Java.Interop.Export\Java.Interop.Export.csproj" />
2022
</ItemGroup>
2123

2224
<ItemGroup>

samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,62 @@
11
<Project>
22

3+
<PropertyGroup>
4+
<DotnetToolPath>$(DOTNET_HOST_PATH)</DotnetToolPath>
5+
<UtilityOutputFullPath>$(MSBuildThisFileDirectory)..\..\bin\Debug-net7.0</UtilityOutputFullPath>
6+
</PropertyGroup>
7+
8+
<Target Name="_CreateJavaCallableWrappers"
9+
Condition=" '$(TargetPath)' != '' "
10+
BeforeTargets="BuildNativeAOTFromJNIJar"
11+
Inputs="$(TargetPath)"
12+
Outputs="$(IntermediateOutputPath)java\.stamp">
13+
<RemoveDir Directories="$(IntermediateOutputPath)java" />
14+
<MakeDir Directories="$(IntermediateOutputPath)java" />
15+
<ItemGroup>
16+
<!-- I can't find a good way to trim the trailing `\`, so append with `.` so we can sanely quote for $(_Libpath) -->
17+
<_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" />
18+
</ItemGroup>
19+
<PropertyGroup>
20+
<_JcwGen>"$(UtilityOutputFullPath)/jcw-gen.dll"</_JcwGen>
21+
<_Target>--codegen-target JavaInterop1</_Target>
22+
<_Output>-o "$(IntermediateOutputPath)/java"</_Output>
23+
<_Libpath>@(_RefAsmDirs->'-L "%(Identity)"', ' ')</_Libpath>
24+
</PropertyGroup>
25+
<Exec Command="$(DotnetToolPath) $(_JcwGen) &quot;$(TargetPath)&quot; $(_Target) $(_Output) $(_Libpath)" />
26+
<Touch Files="$(IntermediateOutputPath)java\.stamp" AlwaysCreate="True" />
27+
</Target>
28+
29+
<Target Name="_AddMarshalMethods"
30+
Condition=" '$(TargetPath)' != '' "
31+
Inputs="$(TargetPath)"
32+
Outputs="$(IntermediateOutputPath).added-marshal-methods"
33+
AfterTargets="_CreateJavaCallableWrappers">
34+
<ItemGroup>
35+
<!-- I can't find a good way to trim the trailing `\`, so append with `.` so we can sanely quote for $(_Libpath) -->
36+
<_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" />
37+
</ItemGroup>
38+
<PropertyGroup>
39+
<_JnimarshalmethodGen>"$(UtilityOutputFullPath)/jnimarshalmethod-gen.dll"</_JnimarshalmethodGen>
40+
<_Verbosity>-v -v --keeptemp</_Verbosity>
41+
<_Libpath>-L "$(TargetDir)" @(_RefAsmDirs->'-L "%(Identity)"', ' ')</_Libpath>
42+
<!-- <_Output>-o "$(IntermediateOutputPath)/jonp"</_Output> -->
43+
</PropertyGroup>
44+
45+
<Exec Command="$(DotnetToolPath) $(_JnimarshalmethodGen) &quot;$(TargetPath)&quot; $(_Verbosity) $(_Libpath)" />
46+
47+
<Touch Files="$(IntermediateOutputPath).added-marshal-methods" AlwaysCreate="True" />
48+
</Target>
49+
350
<Target Name="BuildNativeAOTFromJNIJar"
4-
BeforeTargets="Build"
51+
AfterTargets="Build"
552
Inputs="@(HelloNativeAOTFromJNIJar)"
653
Outputs="$(OutputPath)hello-from-java.jar">
754
<MakeDir Directories="$(IntermediateOutputPath)h-classes" />
855
<ItemGroup>
56+
<_JcwSource Include="$(IntermediateOutputPath)java\**\*.java" />
57+
</ItemGroup>
58+
<ItemGroup>
59+
<_Source Include="@(_JcwSource->Replace('%5c', '/'))" />
960
<_Source Include="@(HelloNativeAOTFromJNIJar->Replace('%5c', '/'))" />
1061
</ItemGroup>
1162
<WriteLinesToFile

samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs renamed to samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Hello_NativeAOTFromJNI;
66

7-
static class JNIEnvInit
7+
static class JavaInteropRuntime
88
{
99
static JniRuntime? runtime;
1010

@@ -30,15 +30,9 @@ static void JNI_Onload (IntPtr vm, IntPtr reserved)
3030
runtime?.Dispose ();
3131
}
3232

33-
// symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h`
34-
[UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_NativeAOTInit_sayHello")]
35-
static IntPtr sayHello (IntPtr jnienv, IntPtr klass)
33+
[UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_java_1interop_JavaInteropRuntime_init")]
34+
static void init ()
3635
{
37-
var s = $"Hello from .NET NativeAOT!";
38-
Console.WriteLine (s);
39-
var h = JniEnvironment.Strings.NewString (s);
40-
var r = JniEnvironment.References.NewReturnToJniRef (h);
41-
JniObjectReference.Dispose (ref h);
42-
return r;
36+
Console.Error.WriteLine ($"C# init()");
4337
}
4438
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Example;
2+
3+
using Java.Interop;
4+
5+
[JniTypeSignature ("example/ManagedType")]
6+
class ManagedType : Java.Lang.Object {
7+
8+
public ManagedType ()
9+
{
10+
}
11+
12+
[JavaCallable ("getString")]
13+
public Java.Lang.String GetString ()
14+
{
15+
return new Java.Lang.String ("Hello from C#, via Java.Interop!");
16+
}
17+
}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package com.microsoft.hello_from_jni;
22

3+
import com.microsoft.java_interop.JavaInteropRuntime;
4+
import example.ManagedType;
5+
36
class App {
47

58
public static void main(String[] args) {
69
System.out.println("Hello from Java!");
7-
String s = NativeAOTInit.sayHello();
10+
JavaInteropRuntime.init();
11+
String s = sayHello();
812
System.out.println("String returned to Java: " + s);
13+
ManagedType mt = new ManagedType();
14+
System.out.println("mt.getString()=" + mt.getString());
915
}
16+
17+
static native String sayHello();
1018
}

samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.microsoft.java_interop;
2+
3+
public class JavaInteropRuntime {
4+
static {
5+
System.loadLibrary("Hello-NativeAOTFromJNI");
6+
}
7+
8+
private JavaInteropRuntime() {
9+
}
10+
11+
public static native void init();
12+
}

src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq.Expressions;
5+
using System.Runtime.InteropServices;
56
using System.Text;
67

78
using Java.Interop;
@@ -187,6 +188,15 @@ public TypeDefinition CreateMarshalMethodDelegateType (string delegateName, ILis
187188
);
188189
delegateDef.BaseType = DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (MulticastDelegate));
189190

191+
var ufpCtor = typeof (UnmanagedFunctionPointerAttribute).GetConstructor (new[]{typeof (CallingConvention)});
192+
var ufpCtorRef = DeclaringAssemblyDefinition.MainModule.ImportReference (ufpCtor);
193+
var ufpAttr = new CustomAttribute (ufpCtorRef);
194+
ufpAttr.ConstructorArguments.Add (
195+
new CustomAttributeArgument (
196+
DeclaringAssemblyDefinition.MainModule.ImportReference (typeof (CallingConvention)),
197+
CallingConvention.Winapi));
198+
delegateDef.CustomAttributes.Add (ufpAttr);
199+
190200
var delegateCtor = new MethodDefinition (
191201
name: ".ctor",
192202
attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
@@ -218,7 +228,7 @@ public void Write (string path)
218228
var module = DeclaringAssemblyDefinition.MainModule;
219229

220230
var c = new MemoryStream ();
221-
DeclaringAssemblyDefinition.Write (c);
231+
DeclaringAssemblyDefinition.Write (path);
222232
c.Position = 0;
223233

224234
if (KeepTemporaryFiles) {
@@ -227,6 +237,10 @@ public void Write (string path)
227237
c.Position = 0;
228238
}
229239

240+
241+
#if false
242+
// `Failed to resolve System.Runtime.InteropServices.CallingConvention`
243+
// because `System.Runtime.InteropServices` is not referenced.
230244
Logger (TraceLevel.Verbose, $"# jonp: ---");
231245

232246
var rp = new ReaderParameters {
@@ -277,6 +291,7 @@ public void Write (string path)
277291
module.AssemblyReferences.Remove (selfRef);
278292
}
279293
newAsm.Write (path);
294+
#endif // false
280295
}
281296

282297
static AssemblyNameReference GetSystemRuntimeReference ()

src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,10 @@ void AddNestedTypes (TypeDefinition type)
164164
var baseRegisteredMethod = GetBaseRegisteredMethod (minfo);
165165
if (baseRegisteredMethod != null)
166166
AddMethod (baseRegisteredMethod, minfo);
167-
else if (minfo.AnyCustomAttributes (typeof(ExportFieldAttribute))) {
167+
else if (minfo.AnyCustomAttributes ("Java.Interop.JavaCallableAttribute")) {
168+
AddMethod (null, minfo);
169+
HasExport = true;
170+
} else if (minfo.AnyCustomAttributes (typeof(ExportFieldAttribute))) {
168171
AddMethod (null, minfo);
169172
HasExport = true;
170173
} else if (minfo.AnyCustomAttributes (typeof (ExportAttribute))) {
@@ -412,6 +415,12 @@ ExportAttribute ToExportAttribute (CustomAttribute attr, IMemberDefinition decla
412415
return new ExportAttribute (name) {ThrownNames = thrown, SuperArgumentsString = superArgs};
413416
}
414417

418+
ExportAttribute ToExportAttributeFromJavaCallableAttribute (CustomAttribute attr, IMemberDefinition declaringMember)
419+
{
420+
var name = attr.ConstructorArguments.Count > 0 ? (string) attr.ConstructorArguments [0].Value : declaringMember.Name;
421+
return new ExportAttribute (name);
422+
}
423+
415424
internal static ExportFieldAttribute ToExportFieldAttribute (CustomAttribute attr)
416425
{
417426
return new ExportFieldAttribute ((string) attr.ConstructorArguments [0].Value);
@@ -447,7 +456,8 @@ static IEnumerable<RegisterAttribute> GetMethodRegistrationAttributes (Mono.Ceci
447456

448457
IEnumerable<ExportAttribute> GetExportAttributes (IMemberDefinition p)
449458
{
450-
return GetAttributes<ExportAttribute> (p, a => ToExportAttribute (a, p));
459+
return GetAttributes<ExportAttribute> (p, a => ToExportAttribute (a, p))
460+
.Concat (GetAttributes<ExportAttribute> (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p)));
451461
}
452462

453463
static IEnumerable<ExportFieldAttribute> GetExportFieldAttributes (Mono.Cecil.ICustomAttributeProvider p)
@@ -458,7 +468,13 @@ static IEnumerable<ExportFieldAttribute> GetExportFieldAttributes (Mono.Cecil.IC
458468
static IEnumerable<TAttribute> GetAttributes<TAttribute> (Mono.Cecil.ICustomAttributeProvider p, Func<CustomAttribute, TAttribute?> selector)
459469
where TAttribute : class
460470
{
461-
return p.GetCustomAttributes (typeof (TAttribute))
471+
return GetAttributes (p, typeof (TAttribute).FullName, selector);
472+
}
473+
474+
static IEnumerable<TAttribute> GetAttributes<TAttribute> (Mono.Cecil.ICustomAttributeProvider p, string attributeName, Func<CustomAttribute, TAttribute?> selector)
475+
where TAttribute : class
476+
{
477+
return p.GetCustomAttributes (attributeName)
462478
.Select (selector)
463479
.Where (v => v != null)
464480
.Select (v => v!);

tools/jnimarshalmethod-gen/App.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,11 @@ void ProcessAssemblies (List<string> assemblies)
228228
// loadContext = CreateLoadContext ();
229229
AppDomain.CurrentDomain.AssemblyResolve += (o, e) => {
230230
Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {e.Name}");
231+
var name = new AssemblyName (e.Name);
231232
foreach (var d in resolver.SearchDirectories) {
232-
var a = Path.Combine (d, e.Name);
233+
var a = Path.Combine (d, name.Name);
233234
var f = a + ".dll";
235+
Log (TraceLevel.Verbose, $"# jonp: checking for: {f} ({File.Exists (f)})");
234236
if (File.Exists (f)) {
235237
return Assembly.LoadFile (Path.GetFullPath (f));
236238
}
@@ -239,6 +241,7 @@ void ProcessAssemblies (List<string> assemblies)
239241
return Assembly.LoadFile (Path.GetFullPath (f));
240242
}
241243
}
244+
Log (TraceLevel.Verbose, $"# jonp: could not resolve assembly: {e.Name}");
242245
return null;
243246
};
244247

0 commit comments

Comments
 (0)