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

[jnienv-gen] fix p/invoke usage for .NET framework #460

Merged
merged 3 commits into from
Aug 12, 2019

Conversation

jonathanpeppers
Copy link
Member

I recently attempted to use Java.Interop from a full .NET framework
console application on Windows.

We don't currently build java-interop.dll for Windows, so I:

  • Took C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Xamarin\Android\libmono-android.release.dll
    and just renamed it to java-interop.dll.
  • Since this is a 64-bit binary, I made the .NET framework project
    targeting x64 only (it was not AnyCPU).
  • I added java-interop.dll as a Content build action.

My console app was attempting to run the main method of r8.jar:

var builder = new JreRuntimeOptions {
    JvmLibraryPath = @"C:\Users\jopepper\android-toolchain\jdk\jre\bin\server\jvm.dll",
    MarshalMemberBuilder = new ProxyMarshalMemberBuilder (),
    ObjectReferenceManager = new ProxyObjectReferenceManager (),
    ValueManager = new ProxyValueManager (),
    TypeManager = new ProxyTypeManager (),
};

builder.ClassPath.Add (@"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Xamarin\Android\r8.jar");

using (var jre = builder.CreateJreVM ()) {
    var @string = new JniType ("java/lang/String");
    var swissArmyKnife = new JniType ("com.android.tools.r8.SwissArmyKnife");
    var main = swissArmyKnife.GetStaticMethod ("main", "([Ljava/lang/String;)V");

    var help = JniEnvironment.Strings.NewString ("--help");
    var args = JniEnvironment.Arrays.NewObjectArray (1, @string.PeerReference, help);
    var __args = stackalloc JniArgumentValue [1];
    __args [0] = new JniArgumentValue (args);
    JniEnvironment.StaticMethods.CallStaticVoidMethod (swissArmyKnife.PeerReference, main, __args);
}

Unfortunately this code crashes at runtime with a cryptic error on any
p/invoke using JniArgumentValue*:

System.Runtime.InteropServices.MarshalDirectiveException:
    Cannot marshal 'parameter #5': Pointers cannot reference marshaled structures.  Use ByRef instead.

This seems like a limitation of .NET framework...

However, it seems to work fine if we use IntPtr instead and just
cast any JniArgumentValue* values to IntPtr.

So for example, the p/invoke can change to:

[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)]
internal static extern unsafe jobject java_interop_jnienv_call_object_method_a (IntPtr jnienv, out IntPtr thrown, jobject instance, IntPtr method, IntPtr args);

args used to be a JniArgumentValue*. Other generated methods need
a cast, such as:

public static unsafe JniObjectReference CallObjectMethod (JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args)
{
    ...
    IntPtr thrown;
    var tmp = NativeMethods.java_interop_jnienv_call_object_method_a (JniEnvironment.EnvironmentPointer, out thrown, instance.Handle, method.ID, (IntPtr) args);
    ...
}

After this, my .NET framework console app was able to start, and it
printed r8 --help output.

I recently attempted to use Java.Interop from a full .NET framework
console application on Windows.

We don't currently build `java-interop.dll` for Windows, so I:

* Took `C:\Program Files (x86)\Microsoft Visual
  Studio\2019\Enterprise\MSBuild\Xamarin\Android\libmono-android.release.dll`
  and just renamed it to `java-interop.dll`.
* Since this is a 64-bit binary, I made the .NET framework project
  targeting `x64` only (it was *not* `AnyCPU`).
* I added `java-interop.dll` as a `Content` build action.

My console app was attempting to run the `main` method of `r8.jar`:

    var builder = new JreRuntimeOptions {
        JvmLibraryPath = @"C:\Users\jopepper\android-toolchain\jdk\jre\bin\server\jvm.dll",
        MarshalMemberBuilder = new ProxyMarshalMemberBuilder (),
        ObjectReferenceManager = new ProxyObjectReferenceManager (),
        ValueManager = new ProxyValueManager (),
        TypeManager = new ProxyTypeManager (),
    };

    builder.ClassPath.Add (@"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Xamarin\Android\r8.jar");

    using (var jre = builder.CreateJreVM ()) {
        var @string = new JniType ("java/lang/String");
        var swissArmyKnife = new JniType ("com.android.tools.r8.SwissArmyKnife");
        var main = swissArmyKnife.GetStaticMethod ("main", "([Ljava/lang/String;)V");

        var help = JniEnvironment.Strings.NewString ("--help");
        var args = JniEnvironment.Arrays.NewObjectArray (1, @string.PeerReference, help);
        var __args = stackalloc JniArgumentValue [1];
        __args [0] = new JniArgumentValue (args);
        JniEnvironment.StaticMethods.CallStaticVoidMethod (swissArmyKnife.PeerReference, main, __args);
    }

Unfortunately this code crashes at runtime with a cryptic error on any
p/invoke using `JniArgumentValue*`:

    System.Runtime.InteropServices.MarshalDirectiveException:
        Cannot marshal 'parameter dotnet#5': Pointers cannot reference marshaled structures.  Use ByRef instead.

This seems like a limitation of .NET framework...

However, it seems to work fine if we use `IntPtr` instead and just
cast any `JniArgumentValue*` values to `IntPtr`.

So for example, the p/invoke can change to:

    [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)]
    internal static extern unsafe jobject java_interop_jnienv_call_object_method_a (IntPtr jnienv, out IntPtr thrown, jobject instance, IntPtr method, IntPtr args);

`args` used to be a `JniArgumentValue*`. Other generated methods need
a cast, such as:

    public static unsafe JniObjectReference CallObjectMethod (JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args)
    {
        ...
        IntPtr thrown;
        var tmp = NativeMethods.java_interop_jnienv_call_object_method_a (JniEnvironment.EnvironmentPointer, out thrown, instance.Handle, method.ID, (IntPtr) args);
        ...
    }

After this, my .NET framework console app was able to start, and it
printed `r8 --help` output.
@jonathanpeppers
Copy link
Member Author

So looks like gendarme is reporting errors:

mono64 --debug=casts lib/gendarme-2.10/gendarme.exe --html gendarme.html --xml gendarme.xml --ignore gendarme-ignore.txt bin/GendarmeDebug/netstandard2.0/Java.Interop.dll
Gendarme v2.10.0.0
Copyright (C) 2005-2011 Novell, Inc. and contributors

Initialization: 0.9 seconds
Java.Interop.dll: 0.8 seconds
TearDown: <0.1 seconds

One assembly processed in 1.7 seconds.
31 defects found. Reports written to: `gendarme.xml',`gendarme.html'.

I'll check that out on macOS. I can also make gendarme.html an artifact or something so we can read it easily in the future.

Some parameters changed, so we have to update `gendarme-ignore.txt`.
@jonathanpeppers
Copy link
Member Author

I fixed up the build definition, too; so we'll have these next time:

image

@jonpryor
Copy link
Member

As part of this PR, please update tests/invocation-overhead/jni.cs by running:

$ make -C tests/invocation-overhead

That will implicitly update tests/invocation-overhead/jni.cs.

Additionally, please apply the following patch to tests:

diff --git a/tests/invocation-overhead/Makefile b/tests/invocation-overhead/Makefile
index 66f61d7..8f58b59 100644
--- a/tests/invocation-overhead/Makefile
+++ b/tests/invocation-overhead/Makefile
@@ -9,6 +9,7 @@ clean:
 
 include ../../build-tools/scripts/mono.mk
 include ../../build-tools/scripts/jdk.mk
+include ../../bin/BuildDebug/JdkInfo.mk
 include ../../build-tools/scripts/msbuild.mk
 
 $(JNIENV_GEN):

@jonathanpeppers
Copy link
Member Author

There must be something else I need to fix here:

jni.cs(15668,19): error CS1594: Delegate `Java.Interop.XAIntPtrs.JniAction_JNIEnvPtr_jobject_IntPtr_JniArgumentValuePtr' has some invalid arguments
jni.cs(15668,83): error CS1503: Argument `#4' cannot convert `System.IntPtr' expression to type `Java.Interop.JniArgumentValue*'

The line is:

var __info = JniEnvironment.CurrentInfo;
__info.Invoker.CallStaticVoidMethodA (__info.EnvironmentPointer, type, method, (IntPtr) args);

Looking into it.

* `jnienv-gen` needs to declare `JniArgumentValue*` as `IntPtr` for
  delegate types, too.
* Generated a new `jni.cs` -- which now compiles
* Updated `Makefile`
@jonpryor jonpryor merged commit 44ccd13 into dotnet:master Aug 12, 2019
@jonathanpeppers jonathanpeppers deleted the full-framework-pinvoke branch September 7, 2023 21:54
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants