Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2ee9b33
[jp_context] added additional code path to treat condition of Java8
marscher Sep 26, 2025
b31d7e9
add test job (only single case) for win and lnx
marscher Sep 26, 2025
06af94c
pass a variable for test selection
marscher Sep 26, 2025
2c415cc
f python.version
marscher Sep 26, 2025
005ff1a
another shot at the variable.
marscher Sep 26, 2025
185549c
azure my a..
marscher Sep 26, 2025
bc79934
azure my a..2
marscher Sep 26, 2025
e789943
access via parameters
marscher Sep 26, 2025
8c6f5e5
diff between build and runtime jdk
marscher Sep 26, 2025
d485cf2
add test
marscher Sep 26, 2025
47680bd
grammar
marscher Sep 26, 2025
a943bbd
param
marscher Sep 26, 2025
3308408
template param
marscher Sep 26, 2025
758459a
wip
marscher Sep 26, 2025
3e5b8b2
wip2
marscher Sep 26, 2025
34ee17d
directly invoke java to obtain version instead of starting a jvm in p…
marscher Sep 26, 2025
e01ae9a
directly invoke java to obtain version instead of starting a jvm in p…
marscher Sep 26, 2025
3add694
tmpl
marscher Sep 26, 2025
23fed6f
parse java -version output
marscher Sep 26, 2025
aa6f94b
parse java -version output, f
marscher Sep 26, 2025
d58c6bc
try to simplify or complicate things
marscher Sep 26, 2025
7d5fee1
try to simplify or complicate things2
marscher Sep 26, 2025
fd94d4f
eat this bitch
marscher Sep 26, 2025
1036a29
omg
marscher Sep 26, 2025
757fcb0
added jniargs class
marscher Sep 30, 2025
fd578d1
obtain version with jvm creation via jni in a separate process.
marscher Sep 30, 2025
266a3f4
fix
marscher Sep 30, 2025
07e1501
wip on azure
marscher Sep 30, 2025
65b3cbc
wip on azure2
marscher Sep 30, 2025
308fe6e
cleanup
marscher Sep 30, 2025
d2d4853
fix fix_startup
marscher Sep 30, 2025
ca29368
test
marscher Sep 30, 2025
c6a8f64
doh
marscher Sep 30, 2025
7f3069d
clean up
marscher Sep 30, 2025
a57f7a2
Update test_startup.py
marscher Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .azure/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,27 @@ jobs:
imageName: "windows-2022"
python.version: '3.12'
jdk.version: '21'
# OSX, we only test an old Python version with JDK8 and recent Py with recent JDK.
# OSX, we only test an old and new Python/Java version combination for the sake of CI runtime.
mac_py39_jdk11:
imageName: "macos-13"
python.version: '3.9'
jpypetest.fast: 'true'
pytest_suppl_args: '--fast'
jdk.version: '11'
mac_py312_jdk17:
imageName: "macos-13"
python.version: '3.12'
jpypetest.fast: 'true'
pytest_suppl_args: '--fast'
jdk.version: '17'

# special
java8_deprecation:
imageName: "ubuntu-latest"
jdk.version: '11'
java_runtime: '8'
python.version: '3.12'
pytest_suppl_args: "-k test_raise_for_java8"
pool:
vmImage: $(imageName)
steps:

- template: scripts/deps.yml
- template: scripts/test.yml

Expand Down
25 changes: 15 additions & 10 deletions .azure/scripts/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# This task tests individual platforms and versions
parameters:
- name: pytest_suppl_args
type: string
default: ""
- name: java_runtime
type: string
# to be expanded later in the template compilation via the runtime variable.
default: "$(jdk.version)"

steps:

- template: python.yml
Expand All @@ -12,30 +21,26 @@ steps:
- template: sdist.yml

- script: |
python -m pip install -e .
python -m pip install -v -e .
displayName: 'Build/install module'

- script: |
# todo: numpy prior execution to build specific paths?
pip install numpy jedi typing_extensions
python -c "import jpype"
displayName: 'Check module'

# install testing dependencies
- script: |
pip install setuptools
python setup.py test_java
pip install -r test-requirements.txt
displayName: 'Install test'

# execute pytest
- script: |
python -m pip install -U pytest
python -m pytest -v --junit-xml=build/test/test.xml test/jpypetest --checkjni
displayName: 'Test JDK $(jdk.version) and Python $(python.version)'
condition: eq(variables['jpypetest.fast'], 'false')

- script: |
python -m pytest -v --junit-xml=build/test/test.xml test/jpypetest --checkjni --fast
displayName: 'Test JDK $(jdk.version) and Python $(python.version) (fast)'
condition: eq(variables['jpypetest.fast'], 'true')
python -m pytest -v --junit-xml=build/test/test.xml test/jpypetest --checkjni ${{ parameters.pytest_suppl_args }}
displayName: "Test JDK $(jdk.version) and Python $(python.version)"

# presence of jpype/ seems to confuse entry_points so `cd` elsewhere
- script: |
Expand Down
2 changes: 2 additions & 0 deletions native/common/include/jp_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ class JPContext

// This will gather C++ resources to clean up after shutdown.
std::list<JPResource*> m_Resources;
// TODO: this is actually not dependent on jpcontext at all and should go somewhere else
static PyObject* getJVMVersion(PyObject* self, PyObject* args);
} ;

extern "C" JPContext* JPContext_global;
Expand Down
190 changes: 151 additions & 39 deletions native/common/jp_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,64 @@
#include <errno.h>
#endif

// minimum JNI API (or equivalent JRE version).
#define USE_JNI_VERSION JNI_VERSION_9

JPResource::~JPResource() = default;
namespace {

/**
* Handles the memory padding and resource handling for JVM arguments.
* The JVM seems to take over the memory after it has been passed.
*/
class JNIArgs {
private:
JavaVMInitArgs* m_JniArgs = nullptr;
public:
JNIArgs(const StringVector& args, bool ignoreUnrecognized) {
// Determine the memory requirements
#define PAD(x) ((x+31)&~31)
size_t mem = PAD(sizeof(JavaVMInitArgs));
size_t oblock = mem;
mem += PAD(sizeof(JavaVMOption)*args.size() + 1);
size_t sblock = mem;
for (const auto & arg : args)
{
mem += PAD(arg.size()+1);
}

// Pack the arguments
JP_TRACE("Pack arguments");
auto block = (char*) malloc(mem);
m_JniArgs = (JavaVMInitArgs*) block; // pointer alias.
memset(m_JniArgs, 0, mem);
m_JniArgs->options = (JavaVMOption*)(&block[oblock]);

// prepare this ...
m_JniArgs->version = USE_JNI_VERSION; // enforce minimum JNI (Java Runtime) version.
m_JniArgs->ignoreUnrecognized = ignoreUnrecognized;
JP_TRACE("IgnoreUnrecognized", ignoreUnrecognized);

m_JniArgs->nOptions = (jint) args.size();
JP_TRACE("NumOptions", m_JniArgs->nOptions);
size_t j = sblock;
for (size_t i = 0; i < args.size(); i++)
{
JP_TRACE("Option", args[i]);
strncpy(&block[j], args[i].c_str(), args[i].size());
m_JniArgs->options[i].optionString = (char*) &block[j];
j += PAD(args[i].size()+1);
}
}
void* getArgs() {
return m_JniArgs;
}
~JNIArgs() {
free(m_JniArgs);
}
};
}

#define USE_JNI_VERSION JNI_VERSION_1_4
JPResource::~JPResource() = default;

void JPRef_failed()
{
Expand Down Expand Up @@ -119,56 +172,38 @@ void JPContext::startJVM(const string& vmPath, const StringVector& args,
throw;
}

// Determine the memory requirements
#define PAD(x) ((x+31)&~31)
size_t mem = PAD(sizeof(JavaVMInitArgs));
size_t oblock = mem;
mem += PAD(sizeof(JavaVMOption)*args.size() + 1);
size_t sblock = mem;
for (size_t i = 0; i < args.size(); i++)
{
mem += PAD(args[i].size()+1);
}

// Pack the arguments
JP_TRACE("Pack arguments");
char *block = (char*) malloc(mem);
JavaVMInitArgs* jniArgs = (JavaVMInitArgs*) block;
memset(jniArgs, 0, mem);
jniArgs->options = (JavaVMOption*)(&block[oblock]);

// prepare this ...
jniArgs->version = USE_JNI_VERSION;
jniArgs->ignoreUnrecognized = ignoreUnrecognized;
JP_TRACE("IgnoreUnrecognized", ignoreUnrecognized);

jniArgs->nOptions = (jint) args.size();
JP_TRACE("NumOptions", jniArgs->nOptions);
size_t j = sblock;
for (size_t i = 0; i < args.size(); i++)
{
JP_TRACE("Option", args[i]);
strncpy(&block[j], args[i].c_str(), args[i].size());
jniArgs->options[i].optionString = (char*) &block[j];
j += PAD(args[i].size()+1);
}
// prepare JNIArgs for JVM.
JNIArgs jniArgs(args, ignoreUnrecognized);

// Launch the JVM
JNIEnv* env = nullptr;
JP_TRACE("Create JVM");
try
{
CreateJVM_Method(&m_JavaVM, (void**) &env, (void*) jniArgs);
CreateJVM_Method(&m_JavaVM, (void**) &env, (void*) jniArgs.getArgs());
} catch (...)
{
JP_TRACE("Exception in CreateJVM?");
}
JP_TRACE("JVM created");
free(jniArgs);

if (m_JavaVM == nullptr)
{
JP_TRACE("Unable to start");
// If we fail with jni_version 9, we fall back to 1.8,
// if that succeeds, raise a human understandable error msg!
JavaVMInitArgs jniArgs2;
jniArgs2.version = JNI_VERSION_1_8;
jniArgs2.nOptions = 0;
jniArgs2.options = nullptr;
jniArgs2.ignoreUnrecognized = JNI_TRUE;
CreateJVM_Method(&m_JavaVM, (void**) &env, &jniArgs2);
if (m_JavaVM) {
JP_TRACE("Java8 found");
m_JavaVM->DestroyJavaVM();
JP_RAISE(PyExc_RuntimeError, "JPype (version >= 1.6) requires at least Java9 to run, you provided a Java8 JVM.");
}
JP_TRACE("Unable to start");
JP_RAISE(PyExc_RuntimeError, "Unable to start JVM");
}

Expand All @@ -178,6 +213,83 @@ void JPContext::startJVM(const string& vmPath, const StringVector& args,
JP_TRACE_OUT;
}

/**
* Get JVM version from java.lang.System.getProperty('java.version') from a freshly created JVM.
*
* @param jvm_path
* @return version string or empty string in case of error
* note: this boots up a jvm, so we'd need to perform this in a separate process to be able to boot up the jpype jvm in the main process.
* So a Python callee should only get this, once a separate process has been created!
*/
PyObject* JPContext::getJVMVersion(PyObject* self, PyObject* args) {
const char* jvm_path;

// Parse the Python arguments
if (!PyArg_ParseTuple(args, "s", &jvm_path)) {
return nullptr; // error, e.g., wrong argument type
}

// Get the entry points in the shared library
jint(JNICALL * CreateJVM_Method)(JavaVM **pvm, void **penv, void *args);
try
{
JP_TRACE("Load entry points");
JPPlatformAdapter *platform = JPPlatformAdapter::getAdapter();
// Load symbols from the shared library
platform->loadLibrary(jvm_path);
CreateJVM_Method = (jint(JNICALL *)(JavaVM **, void **, void *) )platform->getSymbol("JNI_CreateJavaVM");
} catch (JPypeException& ex)
{
(void) ex;
throw;
}

JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vmArgs;
vmArgs.version = JNI_VERSION_1_8;
vmArgs.nOptions = 0;
vmArgs.options = nullptr;
vmArgs.ignoreUnrecognized = JNI_TRUE;
if (CreateJVM_Method(&jvm, (void**) &env, &vmArgs) != 0) {
JP_TRACE("could not create jvm.");
// we failed to create a JVM
PyObject *empty = PyUnicode_FromString("");
return empty;
}

jclass systemClass = env->FindClass("java/lang/System");
JP_TRACE("got system class");
jmethodID getProperty = env->GetStaticMethodID(
systemClass,
"getProperty",
"(Ljava/lang/String;)Ljava/lang/String;"
);

jstring versionStr = (jstring) env->CallStaticObjectMethod(
systemClass,
getProperty,
env->NewStringUTF("java.version")
);

if (versionStr == nullptr) {
JP_TRACE("CallStaticObjectMethod returned null");
} else {
const char* versionCStr = env->GetStringUTFChars(versionStr, nullptr);
if (versionCStr) {
// convert c string to pyobject string
PyObject *py = PyUnicode_FromStringAndSize(versionCStr, strlen(versionCStr));
env->ReleaseStringUTFChars(versionStr, versionCStr);
return py;
} else {
JP_TRACE("Failed to get UTF chars from jstring");
}
}
jvm->DestroyJavaVM();

return nullptr;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None would be better.

}

void JPContext::attachJVM(JNIEnv* env)
{
env->GetJavaVM(&m_JavaVM);
Expand All @@ -187,13 +299,13 @@ void JPContext::attachJVM(JNIEnv* env)
initializeResources(env, false);
}

std::string getShared()
std::string getShared()
{
#ifdef WIN32
// Windows specific
char path[MAX_PATH];
HMODULE hm = NULL;
if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR) &getShared, &hm) != 0 &&
GetModuleFileName(hm, path, sizeof(path)) != 0)
Expand Down
3 changes: 2 additions & 1 deletion native/common/jp_platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class Win32PlatformAdapter : public JPPlatformAdapter
#endif // HPUX
#include <errno.h>

// The code in this modules is mostly excluded from coverage as it is only
// The code in this module is mostly excluded from coverage as it is only
// possible to execute during a fatal error.

class LinuxPlatformAdapter : public JPPlatformAdapter
Expand All @@ -132,6 +132,7 @@ class LinuxPlatformAdapter : public JPPlatformAdapter
{
JP_TRACE("null library");
JP_TRACE("errno", errno);
JP_TRACE("strerror", strerror(errno));
if (errno == ENOEXEC)
{
JP_TRACE("dignostics", dlerror());
Expand Down
2 changes: 2 additions & 0 deletions native/python/pyjp_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,8 @@ static PyObject *PyJPModule_bootstrap(PyObject *module)
#endif

static PyMethodDef moduleMethods[] = {
// obtain jvm version from a new JVM instance. (must be called in separate process or things will horribly go wrong!)
{"_get_jvm_version", (PyCFunction) JPContext::getJVMVersion, METH_VARARGS, ""},
// Startup and initialization
{"isStarted", (PyCFunction) PyJPModule_isStarted, METH_NOARGS, ""},
#ifdef ANDROID
Expand Down
6 changes: 6 additions & 0 deletions setupext/build_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ def build_java_ext(self, ext):
"Jar cache is missing, using --enable-build-jar to recreate it.")

target_version = "11"
# todo: target and source version are used synonymously. consider -release x
"""
warning: [options] location of system modules is not set in conjunction with -source 11
not setting the location of system modules may lead to class files that cannot run on JDK 11
--release 11 is recommended instead of -source 11 -target 11 because it sets the location of system modules automatically
"""
# build the jar
try:
dirname = os.path.dirname(self.get_ext_fullpath("JAVA"))
Expand Down
14 changes: 0 additions & 14 deletions test/jpypetest/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
# See NOTICE file for details.
#
# *****************************************************************************
from functools import lru_cache

import pytest
import _jpype
import jpype
Expand Down Expand Up @@ -152,15 +150,3 @@ def assertElementsAlmostEqual(self, a, b, places=None, msg=None,

def useEqualityFunc(self, func):
return UseFunc(self, func, 'assertEqual')


@lru_cache(1)
def java_version():
import subprocess
import sys
java_version = str(subprocess.check_output([sys.executable, "-c",
"import jpype; jpype.startJVM(); "
"print(jpype.java.lang.System.getProperty('java.version'))"]),
encoding='ascii')
# todo: make this robust for version "numbers" containing strings (e.g.) 22.1-internal
return tuple(map(int, java_version.split(".")))
Loading