Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 23 additions & 6 deletions src/main/c/pemja/core/python_class/pyjobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ pyjobject_init(JNIEnv *env, PyJObject *self)
self->class_name = JcpPyString_FromJString(env, className);
jcpThread = JcpThread_Get();

if (jcpThread->name_to_attrs == NULL) {
jcpThread->name_to_attrs = PyDict_New();
if (jcpThread != NULL) {
if (jcpThread->name_to_attrs == NULL) {
jcpThread->name_to_attrs = PyDict_New();
}
cachedAttrs = PyDict_GetItem(jcpThread->name_to_attrs, self->class_name);
} else {
// On non-pemja threads (e.g., Python async threads), no JcpThread is
// available. Clear the error set by JcpThread_Get() and proceed without
// the per-thread attribute cache.
PyErr_Clear();
cachedAttrs = NULL;
}

cachedAttrs = PyDict_GetItem(jcpThread->name_to_attrs, self->class_name);

if (cachedAttrs == NULL) {
cachedAttrs = PyDict_New();

Expand Down Expand Up @@ -129,8 +136,12 @@ pyjobject_init(JNIEnv *env, PyJObject *self)
}
(*env)->DeleteLocalRef(env, fields);

PyDict_SetItem(jcpThread->name_to_attrs, self->class_name, cachedAttrs);
Py_DECREF(cachedAttrs);
if (jcpThread != NULL) {
PyDict_SetItem(jcpThread->name_to_attrs, self->class_name, cachedAttrs);
Py_DECREF(cachedAttrs);
}
// When jcpThread is NULL, we still own the ref from PyDict_New().
// It will be balanced by the Py_DECREF after self->attr assignment below.
}

if (self->object) {
Expand All @@ -140,6 +151,12 @@ pyjobject_init(JNIEnv *env, PyJObject *self)
self->attr = PyDict_Copy(cachedAttrs);
}

// When there is no JcpThread, no cache holds a reference to cachedAttrs,
// so we must release the owned ref from PyDict_New() here.
if (jcpThread == NULL) {
Py_DECREF(cachedAttrs);
}

(*env)->PopLocalFrame(env, NULL);
return 0;

Expand Down
103 changes: 103 additions & 0 deletions src/main/python/pemja/tests/test_async_thread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
################################################################################
#
# Copyright 2022 Alibaba Group Holding Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
################################################################################

import asyncio
import threading


# ---------------------------------------------------------------------------
# asyncio tests — the event loop runs on a non-pemja thread
# ---------------------------------------------------------------------------

def test_return_custom_object_in_asyncio(java_obj):
"""
Test that calling a Java method which returns a custom Java object
from an asyncio event loop (running on a non-pemja thread) does not
crash the JVM.

Before the fix: SIGSEGV in JcpPyJObject_New (NULL deref on JcpThread).
After the fix: works normally.
"""
result = [None]
error = [None]

async def coro():
# This runs on the event loop thread (a non-pemja thread).
# returnSelf() returns a custom Java object (TestObject), which
# goes through JcpPyJObject_New -> pyjobject_init -> JcpThread_Get().
obj = java_obj.returnSelf()
return str(obj)

def run_loop():
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result[0] = loop.run_until_complete(coro())
loop.close()
except Exception as e:
error[0] = str(e)

t = threading.Thread(target=run_loop)
t.start()
t.join(timeout=10)

if error[0] is not None:
raise RuntimeError("Error in asyncio thread: " + error[0])

return result[0]


def test_return_string_in_asyncio(java_obj):
"""
Control: returning a String (built-in type) from asyncio should always
work, even without the fix.
"""
result = [None]
error = [None]

async def coro():
return java_obj.returnString()

def run_loop():
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result[0] = loop.run_until_complete(coro())
loop.close()
except Exception as e:
error[0] = str(e)

t = threading.Thread(target=run_loop)
t.start()
t.join(timeout=10)

if error[0] is not None:
raise RuntimeError("Error in asyncio thread: " + error[0])

return result[0]


# ---------------------------------------------------------------------------
# Same-thread control test
# ---------------------------------------------------------------------------

def test_return_custom_object_in_same_thread(java_obj):
"""
Control: calling on the pemja thread itself should always work.
"""
obj = java_obj.returnSelf()
return str(obj)
40 changes: 40 additions & 0 deletions src/test/java/pemja/core/PythonInterpreterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,36 @@ public void testCallPythonWithAllTypes() throws Exception {
}
}

@Test
public void testCustomObjectInAsyncThread() {
PythonInterpreterConfig config =
PythonInterpreterConfig.newBuilder().addPythonPaths(testDir).build();
try (PythonInterpreter interpreter = new PythonInterpreter(config)) {
interpreter.exec("import test_async_thread");

TestObject obj = new TestObject();

// Control: returning String from asyncio should always work
Object stringResult =
interpreter.invoke("test_async_thread.test_return_string_in_asyncio", obj);
assertEquals("hello from java", stringResult);

// Control: returning custom object on pemja thread should always work
Object sameThreadResult =
interpreter.invoke(
"test_async_thread.test_return_custom_object_in_same_thread", obj);
assert sameThreadResult != null;

// This is the bug scenario: returning a custom Java object from
// an asyncio event loop (non-pemja thread). Without the fix, this
// crashes the JVM with SIGSEGV in JcpPyJObject_New.
Object asyncResult =
interpreter.invoke(
"test_async_thread.test_return_custom_object_in_asyncio", obj);
assert asyncResult != null;
}
}

@Test
public void testCallPyJObject() {
PythonInterpreterConfig config =
Expand Down Expand Up @@ -925,6 +955,16 @@ public String testJavaCallPython(Interpreter interpreter) {
return interpreter.get("a", String.class);
}
/* -------------------------------------------------------------------------------------- */

/* ----------------------------------- test return custom object ----------------------- */
public TestObject returnSelf() {
return this;
}

public String returnString() {
return "hello from java";
}
/* -------------------------------------------------------------------------------------- */
}

private static void deleteDirectory(File directory) throws IOException {
Expand Down