-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[jcw-gen, jnimarshalmethod-gen] Native method consistency #1160
Conversation
Context: #1153 Context: 4787e01 Context: 77800dd PR #1153 is exploring the use of [.NET Native AOT][0] to produce a native library which is used *within* a `java`-originated process: % java -cp … com/microsoft/hello_from_jni/App # launches NativeAOT-generated native lib, executes C# code… As NativeAOT has no support for `System.Reflection.Emit`, the only way for Java code to invoke managed code -- in a Desktop Java.Base environment! [^0] see 4787e01 -- would be to pre-generate the required marshal methods via `jnimarshalmethod-gen`. This in turn requires updating `jcw-gen` to support the pre-existing `Java.Interop.JavaCallableAttribute`, so that C# code could reasonably declare methods visible to Java, along with the introduction of, and support for, a new `Java.Interop.JavaCallableConstructorAttribute` type. This allows straightforward usage: [JniTypeSignature ("example/ManagedType")] // for a nice Java name! class ManagedType : Java.Lang.Object { int value; [JavaCallableConstructor(SuperConstructorExpression="")] public ManagedType (int value) { this.value = value; } [JavaCallable ("getString")] public Java.Lang.String GetString () { return new Java.Lang.String ($"Hello from C#, via Java.Interop! Value={value}"); } } Run this through `jcw-gen` and `jnimarshalmethod-gen`, run the app, and nothing worked (?!), because not all pieces were in agreement. Java `native` method registration is One Of Those Things™ that involves lots of moving pieces: * `generator` emits bindings for Java types, which includes Java method names, signatures, and (on .NET Android) the "connector method" to use: [Register ("toString", "()Ljava/lang/String;", "GetToStringHandler")] // .NET Android [JniMethodSignature ("toString", "()Ljava/lang/String;")] // Java.Base public override unsafe string? ToString () {…} * `jcw-gen` uses `generator` output, *prefixing* Java method names with `n_` for `native` method declarations, along with a "normal" method wrapper [^1] public String toString() {return n_toString();} private native String n_toString(); * `jnimarshalmethod-gen` emits marshal methods for Java.Base, and needs to register the `native` methods declared by `jcw-gen`. `jnimarshalmethod-gen` and `jcw-gen` need to be consistent with each other. Turns Out, `jcw-gen` and `jnimarshalmethod-gen` were *not* consistent. The only "real" `jnimarshalmethod-gen` usage (77800dd) is with the `Java.Interop.Export-Tests` unit test assembly, which *did not use* `jcw-gen`; it contained only hand-written Java code. Consequently, *none* of the Java `native` methods declared within it had an `n_` prefix, and since this worked with `jnimarshalmethod-gen`, this means that `jnimarshalmethod-gen` registration logic likewise didn't use `n_` prefixed method names. The result is that in the NativeAOT app, it would attempt to register the `native` Java method `ManagedType.getString()`, while what `jcw-gen` declared was `ManagedType.n_getString()`! Java promptly threw an exception, and the app crashed. Update `Java.Interop.Export-Tests` so that all the methods used with `MarshalMemberBuilder` are declared with `n_` prefixes, and add a `Java.Lang.Object` subclass example to the unit tests: Update `tests/Java.Interop.Tools.JavaCallableWrappers-Tests` to add a test for `.CodeGenerationTarget==JavaInterop1`. Add `$(NoWarn)` to `Java.Interop.Tools.JavaCallableWrappers-Tests.csproj` in order to "work around" warnings-as-errors: …/src/Java.Interop.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs(19,63): error CA1019: Remove the property setter from Name or reduce its accessibility because it corresponds to positional argument name …/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs(53,4): error CA1019: Remove the property setter from Name or reduce its accessibility because it corresponds to positional argument name …/src/Java.Interop.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs(12,16): error CA1813: Avoid unsealed attributes … These are "weird"; the warnings/errors appear to come in because `Java.Interop.Tools.JavaCallableWrappers-Tests.csproj` now has: <Compile Include="..\..\src\Java.Interop\Java.Interop\JniTypeSignatureAttribute.cs" /> which appears to pull in `src/Java.Interop/.editorconfig`, which makes CA1019 and CA1813 errors. (I do not understand what is happening.) Update `jnimarshalmethod-gen` so that the Java `native` methods it registers have an `n_` prefix. Refactor `ExpressionAssemblyBuilder.CreateRegistrationMethod()` to `ExpressionAssemblyBuilder.AddRegistrationMethod()`, so that the `EmitConsoleWriteLine()` invocation can provide the *full* type name of the `__RegisterNativeMembers()` method, which helps when there is more than one such method running around… Update `ExpressionAssemblyBuilder` so that the delegate types it creates for marshal method registration all have `[UnmanagedFunctionPointer(CallingConvention.Winapi)]`. (This isn't needed *here*, but is needed in the context of NativeAOT, as NativeAOT will only emit "marshal stubs" for delegate types which have `[UnmanagedFunctionPointer]`.) Unfortunately, adding `[UnmanagedFunctionPointer]` broke things: error JM4006: jnimarshalmethod-gen: Unable to process assembly '…/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) … The problem is that `CallingConvention` was resolved from `System.Private.CoreLib`, and when we removed that assembly reference, the `CallingConvention` couldn't be resolved at all. We could "fix" this by explicitly adding a reference to `System.Runtime.InteropServices.dll`, but how many more such corner cases exist? The current approach is not viable. Remove the code from 77800dd which attempts to remove `System.Private.CoreLib`. So long as `ExpressionAssemblyBuilder` output is *only* used in "completed" apps (not distributed in NuGet packages or some "intermediate" form), referencing `System.Private.CoreLib` is "fine". Update `jnimarshalmethod-gen` assembly location probing: in #1153, it was attempting to resolve the *full assembly name* of `Java.Base`, as `Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null`, causing it to attempt to load the file `Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null.dll`, which doesn't exist. Use `AssemblyName` to parse the string and extract out the assembly name, so that `Java.Base.dll` is probed for and found. Update `JreTypeManager` to *also* register the marshal methods generated by `Runtime.MarshalMemberBuilder.GetExportedMemberRegistrations()`. With all that, the updated `Java.Interop.Export-Tests` test now work both before and after `jnimarshalmethod-gen` is run: % dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll && dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll && dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Interop.Export-Tests.dll … TODO: * `generator --codegen-target=JavaInterop1` should emit JNI method signature information for constructors! This would likely remove the need for `[JavaCallableConstructor(SuperConstructorExpression="")`. * #1159 [0]: https://learn.microsoft.com/dotnet/core/deploying/native-aot [^0]: In a .NET Android environment, marshal methods are part of `generator` output, so things would be more straightforward there, though all the `_JniMarshal_*` types that are declared would also need to have `[UnmanagedFunctionPointer(CallingConvention.Winapi)]`… [^1]: Why not just declare `toString()` as `native`? Why have the separate `n_`-prefixed version? To make the `#if MONODROID_TIMING` block more consistent within `JavaCallableWrapperGenerator.GenerateMethod()`. It's likely "too late" to *easily* change this now.
Doh, and a 4th place that needs to be consistent wrt the Will need to update the commit message w/ that tidbit before merging. |
Also need to comment upon the |
The Which means dotnet/android#7554 would be fixed with this PR. |
PR #1161 should go in first, and this should be rebased atop it. |
Context: #1153
Context: 4787e01
Context: 77800dd
PR #1153 is exploring the use of .NET Native AOT to produce a
native library which is used within a
java
-originated process:As NativeAOT has no support for
System.Reflection.Emit
, the onlyway for Java code to invoke managed code -- in a Desktop Java.Base
environment! 1 see 4787e01 -- would be to pre-generate the
required marshal methods via
jnimarshalmethod-gen
.This in turn requires updating
jcw-gen
to support the pre-existingJava.Interop.JavaCallableAttribute
, so that C# code couldreasonably declare methods visible to Java, along with the
introduction of, and support for, a new
Java.Interop.JavaCallableConstructorAttribute
type. This allowsstraightforward usage:
Run this through
jcw-gen
andjnimarshalmethod-gen
, run the app,and nothing worked (?!), because not all pieces were in agreement.
Java
native
method registration is One Of Those Things™ thatinvolves lots of moving pieces:
generator
emits bindings for Java types, which includes Javamethod names, signatures, and (on .NET Android) the "connector
method" to use:
jcw-gen
usesgenerator
output, prefixing Java method nameswith
n_
fornative
method declarations, along with amethod wrapper 2
jnimarshalmethod-gen
emits marshal methods for Java.Base,and needs to register the
native
methods declared byjcw-gen
.jnimarshalmethod-gen
andjcw-gen
need to be consistent witheach other.
MarshalMemberbuilder.CreateMarshalToManagedMethodRegistration()
creates a
JniNativeMethodRegistration
instance which containsthe name of the Java
native
method to register, and wasusing a name inconsistent with
jcw-gen
.Turns Out,
jcw-gen
,jnimarshalmethod-gen
, andMarshalMemberBuilder
were not consistent.The only "real"
jnimarshalmethod-gen
usage (77800dd) is with theJava.Interop.Export-Tests
unit test assembly, which did not usejcw-gen
; it contained only hand-written Java code. Consequently,none of the Java
native
methods declared within it had ann_
prefix, and since this worked with
jnimarshalmethod-gen
, this meansthat
jnimarshalmethod-gen
registration logic likewise didn't usen_
prefixed method names.The result is that in the NativeAOT app, it would attempt to register
the
native
Java methodManagedType.getString()
, while whatjcw-gen
declared wasManagedType.n_getString()
!Java promptly threw an exception, and the app crashed.
Update
Java.Interop.Export-Tests
so that all the methods used withMarshalMemberBuilder
are declared withn_
prefixes, and adda
Java.Lang.Object
subclass example to the unit tests:Update
tests/Java.Interop.Tools.JavaCallableWrappers-Tests
toadd a test for
.CodeGenerationTarget==JavaInterop1
.Add
$(NoWarn)
toJava.Interop.Tools.JavaCallableWrappers-Tests.csproj
in order to"work around" warnings-as-errors:
These are "weird"; the warnings/errors appear to come in because
Java.Interop.Tools.JavaCallableWrappers-Tests.csproj
now includes:which appears to pull in
src/Java.Interop/.editorconfig
, which makesCA1019 and CA1813 errors. (I do not understand what is happening.)
Update
jnimarshalmethod-gen
so that the Javanative
methods itregisters have an
n_
prefix.Refactor
ExpressionAssemblyBuilder.CreateRegistrationMethod()
toExpressionAssemblyBuilder.AddRegistrationMethod()
, so thatthe
EmitConsoleWriteLine()
invocation can provide the fulltype name of the
__RegisterNativeMembers()
method, which helpswhen there is more than one such method running around…
Update
ExpressionAssemblyBuilder
so that the delegate types itcreates for marshal method registration all have
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
. (This isn'tneeded here, but is needed in the context of NativeAOT, as
NativeAOT will only emit "marshal stubs" for delegate types which
have
[UnmanagedFunctionPointer]
.) Unfortunately, adding[UnmanagedFunctionPointer]
broke things:The problem is that
CallingConvention
was resolved fromSystem.Private.CoreLib
, and when we removed that assemblyreference, the
CallingConvention
couldn't be resolved at all.We could "fix" this by explicitly adding a reference to
System.Runtime.InteropServices.dll
, but how many more such cornercases exist? The current approach is not viable.
Remove the code from 77800dd which attempts to remove
System.Private.CoreLib
. So long asExpressionAssemblyBuilder
output is only used in "completed" apps (not distributed in NuGet
packages or some "intermediate" form), referencing
System.Private.CoreLib
is "fine".Update
jnimarshalmethod-gen
assembly location probing: in #1153, itwas attempting to resolve the full assembly name of
Java.Base
, asJava.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null
,causing it to attempt to load the file
Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null.dll
,which doesn't exist. Use
AssemblyName
to parse the string andextract out the assembly name, so that
Java.Base.dll
is probed forand found.
Update
JreTypeManager
to also register the marshal methodsgenerated by
Runtime.MarshalMemberBuilder.GetExportedMemberRegistrations()
.With all that, the updated
Java.Interop.Export-Tests
test now workboth before and after
jnimarshalmethod-gen
is run:TODO:
generator --codegen-target=JavaInterop1
should emitJNI method signature information for constructors!
This would likely remove the need for
[JavaCallableConstructor(SuperConstructorExpression="")
.jnimarshalmethod-gen should be used & usable with
Java.Base-Tests
#1159Footnotes
In a .NET Android environment, marshal methods are part of
generator
output, so things would be more straightforwardthere, though all the
_JniMarshal_*
types that are declaredwould also need to have
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
… ↩Why not just declare
toString()
asnative
? Why have theseparate
n_
-prefixed version? To make the#if MONODROID_TIMING
block more consistent withinJavaCallableWrapperGenerator.GenerateMethod()
.It's likely "too late" to easily change this now. ↩