Repository: mesonbuild/meson

Issue:
JDK system dependency: link against libjvm
**Describe the bug**
`jni.h` does not only contain structures to run c(++) code from java, but also java from c(++). For this to work the resulting binary has to be linked against `libjvm.so` located in `$JAVA_HOME/lib/server/`. Otherwise if your code tries to call these, the linker will fail:
```
FAILED: mjvmtest 
cc  -o mjvmtest mjvmtest.p/jvm.c.o -Wl,--as-needed -Wl,--no-undefined
/usr/lib64/gcc/x86_64-suse-linux/11/../../../../x86_64-suse-linux/bin/ld: mjvmtest.p/jvm.c.o: in function `main':
/home/admin/workspace/cli/meson/mjvmtest/_build/../jvm.c:16: undefined reference to `JNI_CreateJavaVM'
collect2: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.
```

**To Reproduce**
I've taken the JNI Invokation Example: https://docs.oracle.com/en/java/javase/11/docs/specs/jni/invocation.html, ported it to C and made it actually do something.
```c
#include <jni.h>	/* where everything is defined */
#include <stdbool.h>

int main(int, char**) {
	JavaVM *jvm;	/* denotes a Java VM */
	JNIEnv *env;	/* pointer to native method interface */
	JavaVMInitArgs vm_args;	/* JDK/JRE 10 VM initialization arguments */
	JavaVMOption options[2];
	options[0].optionString = "-Djava.class.path=/usr/lib64/java";
	options[1].optionString = "-Xcheck:jni";
	vm_args.version = JNI_VERSION_10;
	vm_args.nOptions = 1;
	vm_args.options = options;
	vm_args.ignoreUnrecognized = false;
	/* load and initialize a Java VM, return a JNI interface
	* pointer in env */
	JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
	/* get stdout PrintStream */
	jclass system = (*env)->FindClass(env, "java/lang/System");
	jfieldID jstdout_id = (*env)->GetStaticFieldID(env, system, "out", "Ljava/io/PrintStream;");
	jobject jstdout = (*env)->GetStaticObjectField(env, system, jstdout_id);
	/* invoke println method of PrintStream */
	jclass print_stream = (*env)->FindClass(env, "java/io/PrintStream");
	jmethodID mid = (*env)->GetMethodID(env, print_stream, "println", "(Ljava/lang/String;)V");
	jstring to_print = (*env)->NewStringUTF(env, "Hello World from C");
	(*env)->CallVoidMethod(env, jstdout, mid, to_print);
	/* We are done. */
	(*jvm)->DestroyJavaVM(jvm);
}
```

```meson
project('mjvmtest', 'c',
	version : '0.1',
	default_options : ['warning_level=3', 'c_std=gnu2x'])

app = executable('mjvmtest', [
		'jvm.c'
	],
	dependencies: [
		#dependency('jdk', version: ['>=11', '<12'])
		dependency('jdk', version: '>=11')
	],
	install : true
)
```

**Expected behavior**
I expect it to link against libjvm.so.

Adding ` self.link_args = [f'{self.java_home}/lib/server/libjvm.so']`to the JDKSystemDependency __init__ method in `mesonbuild/depenencies/dev.py` makes it work.

What needs to be done is to extend it that it uses the correct file extension for each OS and maybe link it in a way that libjvm.so does not have to be in the library search path.


**system parameters**
* OS: openSUSE Tumbleweed 20211117
* Python 3.8.12
* Meson 0.59.4 & cc80187
* Ninja 1.10.2



Discussion:
> extend it that it uses the correct file extension for each OS

Use `self.clib_compiler.find_library('jvm', environment, [f'{self.java_home}/lib/server/'])`

> maybe link it in a way that libjvm.so does not have to be in the library search path.

You can do this from meson.build using the install_rpath kwarg, which has no effect on Windows (where there is no such concept).  meson translates install_rpath kwargs via mesonbuild/linkers/linkers.py in `GnuLikeDynamicLinkerMixin.build_rpath_args()`, there are a couple of special behaviors that meson has but I guess it should be irrelevant if you don't need to cope with `$ORIGIN` support. The more troubling concern is that this behavior is GNU-like, not something that works with generic POSIX linkers -- see how the other linker classes in that file define the `build_rpath_args()` function.

I think meson just passes `-Wl,-rpath` args that come from a pkg-config dependency as is, and assumes they work entirely as expected, but in that case we sort of assume that the installed pkg-config files contain the arguments relevant to the platform default linker.
I was literally just thinking about this since I was reading https://cmake.org/cmake/help/latest/module/FindJNI.html. I was thinking about a way to do this, but I didn't know what libraries are actually meant for linking against in the JDK. Seems like CMake supports `libjvm.so` and `libjawt.so`. Are the others implementation details or something?

One potential interface:

```meson
dependency(
  'jni',
  version: '>= 1.8',
  libraries: [
    'jvm',
    'jawt'
  ]
)
```

Or it could be `libraries: true` for instance.
Looking at the include files. It seems like `jawt` and `jvm` are the only exported ones. When https://github.com/mesonbuild/meson/pull/9879 lands, I will see about solving this hopefully in time for 0.62.0. I think will use the `libraries: true` way initially with it defaulting to `false`. Let me know if someone has a better way :). 
huh, I never used libjawt and so far I didn't feel the need to link against it, since AWT is pretty dead AFAIK :)

it should definitively be optional
@eli-schwartz thoughts?
you can probably ping me in the future with any jdk/java concerns pertaining to this dependency or the Java module fyi
The traditional interface for dependency-specific handling of additional components within a dependency is the `modules: ` kwarg.

So by default behavior would be preserved, don't link to anything. Is there a valid use case for this BTW? Do people always want to link to jvm itself?

Users who want AWT would use `modules: ['awt']`. Users who want the JVM might pass that as a modules kwarg, depending on what we decide the default should be.
I would assume one use case for this is embedding a JVM in another application. Thanks. I will switch to `modules`. The default behavior would stay as "just add the include directories" since most people probably aren't linking to the JVM libraries.
Makes sense. So both jvm and awt should be opt-in modules.
Right. Information around this is pretty hard to come by to be honest. Will have a PR up in a bit

Resolving patch:
diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py
--- a/mesonbuild/dependencies/dev.py
+++ b/mesonbuild/dependencies/dev.py
@@ -15,6 +15,8 @@
 # This file contains the detection logic for external dependencies useful for
 # development purposes, such as testing, debugging, etc..
 
+from __future__ import annotations
+
 import glob
 import os
 import re
@@ -38,6 +40,13 @@
 if T.TYPE_CHECKING:
     from ..envconfig import MachineInfo
     from .. environment import Environment
+    from typing_extensions import TypedDict
+
+    class JNISystemDependencyKW(TypedDict):
+        modules: T.List[str]
+        # FIXME: When dependency() moves to typed Kwargs, this should inherit
+        # from its TypedDict type.
+        version: T.Optional[str]
 
 
 def get_shared_library_suffix(environment: 'Environment', for_machine: MachineChoice) -> str:
@@ -500,8 +509,8 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.
 
 
 class JNISystemDependency(SystemDependency):
-    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
-        super().__init__('jni', environment, kwargs)
+    def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW):
+        super().__init__('jni', environment, T.cast(T.Dict[str, T.Any], kwargs))
 
         self.feature_since = ('0.62.0', '')
 
@@ -512,6 +521,14 @@ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
         self.javac = environment.coredata.compilers[self.for_machine]['java']
         self.version = self.javac.version
 
+        modules: T.List[str] = mesonlib.listify(kwargs.get('modules', []))
+        for module in modules:
+            if module not in {'jvm', 'awt'}:
+                log = mlog.error if self.required else mlog.debug
+                log(f'Unknown JNI module ({module})')
+                self.is_found = False
+                return
+
         if 'version' in kwargs and not version_compare(self.version, kwargs['version']):
             mlog.error(f'Incorrect JDK version found ({self.version}), wanted {kwargs["version"]}')
             self.is_found = False
@@ -530,6 +547,44 @@ def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
         java_home_include = self.java_home / 'include'
         self.compile_args.append(f'-I{java_home_include}')
         self.compile_args.append(f'-I{java_home_include / platform_include_dir}')
+
+        if modules:
+            if m.is_windows():
+                java_home_lib = self.java_home / 'lib'
+                java_home_lib_server = java_home_lib
+            else:
+                if version_compare(self.version, '<= 1.8.0'):
+                    # The JDK and Meson have a disagreement here, so translate it
+                    # over. In the event more translation needs to be done, add to
+                    # following dict.
+                    def cpu_translate(cpu: str) -> str:
+                        java_cpus = {
+                            'x86_64': 'amd64',
+                        }
+
+                        return java_cpus.get(cpu, cpu)
+
+                    java_home_lib = self.java_home / 'jre' / 'lib' / cpu_translate(m.cpu_family)
+                    java_home_lib_server = java_home_lib / "server"
+                else:
+                    java_home_lib = self.java_home / 'lib'
+                    java_home_lib_server = java_home_lib / "server"
+
+            if 'jvm' in modules:
+                jvm = self.clib_compiler.find_library('jvm', environment, extra_dirs=[str(java_home_lib_server)])
+                if jvm is None:
+                    mlog.debug('jvm library not found.')
+                    self.is_found = False
+                else:
+                    self.link_args.extend(jvm)
+            if 'awt' in modules:
+                jawt = self.clib_compiler.find_library('jawt', environment, extra_dirs=[str(java_home_lib)])
+                if jawt is None:
+                    mlog.debug('jawt library not found.')
+                    self.is_found = False
+                else:
+                    self.link_args.extend(jawt)
+
         self.is_found = True
 
     @staticmethod
@@ -553,7 +608,7 @@ def __machine_info_to_platform_include_dir(m: 'MachineInfo') -> T.Optional[str]:
 
 
 class JDKSystemDependency(JNISystemDependency):
-    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
+    def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW):
         super().__init__(environment, kwargs)
 
         self.feature_since = ('0.59.0', '')


Test patch:
diff --git a/test cases/java/9 jni/meson.build b/test cases/java/9 jni/meson.build
--- a/test cases/java/9 jni/meson.build	
+++ b/test cases/java/9 jni/meson.build	
@@ -13,7 +13,7 @@ javamod = import('java')
 
 java = find_program('java')
 
-jni_dep = dependency('jni', version : '>=1.8')
+jni_dep = dependency('jni', version : '>=1.8', modules: ['jvm', 'awt'])
 
 # generate native headers
 subdir('src/com/mesonbuild')