Skip to content
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

jnimarshalmethod-gen should be used & usable with Java.Base-Tests #1159

Closed
jonpryor opened this issue Nov 10, 2023 · 0 comments · Fixed by #1164
Closed

jnimarshalmethod-gen should be used & usable with Java.Base-Tests #1159

jonpryor opened this issue Nov 10, 2023 · 0 comments · Fixed by #1164
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java

Comments

@jonpryor
Copy link
Member

In the same way that jnimarshalmethod-gen is run against Java.Interop.Export-Tests in 77800dd, jnimarshalmethod-gen should also be run against Java.Base-Tests, and Java.Base-Tests should continue passing after the fixup, a'la:

% dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Base-Tests.dll &&
    dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll bin/TestDebug-net7.0/Java.Base-Tests.dll  &&
    dotnet test --logger "console;verbosity=detailed" bin/TestDebug-net7.0/Java.Base-Tests.dll

One problem with this now is that jnimarshalmethod-gen doesn't do anything to Java.Base-Tests.dll, even though it has at least one Java.Lang.Object subclass, and thus requires marshal methods.

@jpobst jpobst added enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java labels Nov 11, 2023
jonpryor added a commit that referenced this issue Nov 11, 2023
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.
jonpryor added a commit that referenced this issue Nov 13, 2023
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
    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.

  * `MarshalMemberbuilder.CreateMarshalToManagedMethodRegistration()`
    creates a `JniNativeMethodRegistration` instance which contains
    the name of the Java `native` method to register, and was
    using a name inconsistent with `jcw-gen`.

Turns Out, `jcw-gen`, `jnimarshalmethod-gen`, and
`MarshalMemberBuilder` 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 includes:

	<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.
jonpryor added a commit that referenced this issue Nov 16, 2023
Fixes: #1159

Context: c93fea0

`jnimarshalmethod-gen` is intended to generate marshal methods for
any type that that:

  * Has `[JavaCallable]` (tested in `Java.Interop.Export-Tests.dll`
    and c93fea0), or
  * Overrides a `virtual` method which has `[JniMethodSignature]`, or
  * Implements an interface method which has `[JniMethodSignature]`.

Thus, the intention was that it should generate marshal methods for
`Java.Base-Tests`:

	% dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll -v bin/TestDebug-net7.0/Java.Base-Tests.dll
	Unable to read assembly 'bin/TestDebug-net7.0/Java.Base-Tests.dll' with symbols. Retrying to load it without them.
	Preparing marshal method assembly 'Java.Base-Tests-JniMarshalMethods'
	Processing Java.BaseTests.JavaInvoker type
	Processing Java.BaseTests.MyRunnable type
	Processing Java.BaseTests.MyIntConsumer type
	Marshal method assembly 'Java.Base-Tests-JniMarshalMethods' created

Notably missing? No messages stating:

	Adding marshal method for …

Also missing?  `ikdasm bin/TestDebug-net7.0/Java.Base-Tests.dll`
showed that there were no `__RegisterNativeMembers()` methods emitted.

The `jnimarshalmethod-gen` invocation was a no-op!

The problems were twofold:

 1. It was only looking for methods with
    `Android.Runtime.RegisterAttribute`.  This was useful for
    Xamarin.Android (when we were trying to make it work), but
    doesn't work with Java.Base.  We need to *also* look for
    `Java.Interop.JniMethodSignature`.

    Relatedly, the attempt to use
    `registerAttribute.Constructor.Parameters` to determine parameter
    names didn't work; the parameter name was always `""`.

 2. A'la c93fea0, we need to ensure that the Java `native` methods
    we register are consistent with `jcw-gen` output.

Fix these two problems, which allows `jnimarshalmethod-gen` to now
emit marshal methods for types within `Java.Base-Tests.dll`.

Additionally, rework the `jnimarshalmethod-gen -f` logic to *remove*
the existing `__<$>_jni_marshal_methods` nested type when present.
jonpryor added a commit that referenced this issue Nov 22, 2023
Fixes: #1159

Context: c93fea0

`jnimarshalmethod-gen` is intended to generate marshal methods for
any type that that:

  * Has `[JavaCallable]` (tested in `Java.Interop.Export-Tests.dll`
    and c93fea0), or
  * Overrides a `virtual` method which has `[JniMethodSignature]`, or
  * Implements an interface method which has `[JniMethodSignature]`.

Thus, the intention was that it should generate marshal methods for
`Java.Base-Tests`:

	% dotnet bin/Debug-net7.0/jnimarshalmethod-gen.dll -v bin/TestDebug-net7.0/Java.Base-Tests.dll
	Unable to read assembly 'bin/TestDebug-net7.0/Java.Base-Tests.dll' with symbols. Retrying to load it without them.
	Preparing marshal method assembly 'Java.Base-Tests-JniMarshalMethods'
	Processing Java.BaseTests.JavaInvoker type
	Processing Java.BaseTests.MyRunnable type
	Processing Java.BaseTests.MyIntConsumer type
	Marshal method assembly 'Java.Base-Tests-JniMarshalMethods' created

Notably missing? No messages stating:

	Adding marshal method for …

Also missing?  `ikdasm bin/TestDebug-net7.0/Java.Base-Tests.dll`
showed that there were no `__RegisterNativeMembers()` methods emitted.

The `jnimarshalmethod-gen` invocation was a no-op!

The problems were twofold:

 1. It was only looking for methods with
    `Android.Runtime.RegisterAttribute`.  This was useful for
    Xamarin.Android (when we were trying to make it work), but
    doesn't work with Java.Base.  We need to *also* look for
    `Java.Interop.JniMethodSignature`.

    Relatedly, the attempt to use
    `registerAttribute.Constructor.Parameters` to determine parameter
    names didn't work; the parameter name was always `""`.

 2. A'la c93fea0, we need to ensure that the Java `native` methods
    we register are consistent with `jcw-gen` output.

Fix these two problems, which allows `jnimarshalmethod-gen` to now
emit marshal methods for types within `Java.Base-Tests.dll`.

Additionally, rework the `jnimarshalmethod-gen -f` logic to *remove*
the existing `__<$>_jni_marshal_methods` nested type when present.
@github-actions github-actions bot locked and limited conversation to collaborators Apr 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants