Skip to content

Commit b993f30

Browse files
committed
[GR-66427] Avoid stashing native exception upon reference poll when not necessary
PullRequest: graalpython/3859
2 parents 2c84835 + ed4349b commit b993f30

File tree

2 files changed

+90
-93
lines changed

2 files changed

+90
-93
lines changed

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/PythonNativeWrapper.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -198,7 +198,6 @@ public final long getRefCount() {
198198
return MANAGED_REFCNT;
199199
}
200200

201-
@TruffleBoundary(allowInlining = true)
202201
public long incRef() {
203202
assert isNative();
204203
long pointer = HandlePointerConverter.pointerToStub(getNativePointer());
@@ -211,7 +210,6 @@ public long incRef() {
211210
return IMMORTAL_REFCNT;
212211
}
213212

214-
@TruffleBoundary(allowInlining = true)
215213
public long decRef() {
216214
assert isNative();
217215
long pointer = HandlePointerConverter.pointerToStub(getNativePointer());

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/transitions/CApiTransitions.java

Lines changed: 89 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -417,96 +417,82 @@ public static int pollReferenceQueue() {
417417
int count = 0;
418418
long start = 0;
419419
NativeObjectReferenceArrayWrapper referencesToBeFreed = handleContext.referencesToBeFreed;
420-
PythonContext.PythonThreadState threadState = context.getThreadState(context.getLanguage());
421-
/*
422-
* There can be an active exception. Since we might be calling arbitary python, we
423-
* need to stash it.
424-
*/
425-
Object savedException = CExtCommonNodes.ReadAndClearNativeException.executeUncached(threadState);
426-
try {
427-
while (true) {
428-
Object entry = queue.poll();
429-
if (entry == null) {
430-
if (count > 0) {
431-
assert handleContext.referenceQueuePollActive;
432-
releaseNativeObjects(referencesToBeFreed);
433-
handleContext.referenceQueuePollActive = false;
434-
LOGGER.fine("collected " + count + " references from native reference queue in " + ((System.nanoTime() - start) / 1000000) + "ms");
435-
}
436-
return manuallyCollected;
437-
}
438-
if (count == 0) {
439-
assert !handleContext.referenceQueuePollActive;
440-
handleContext.referenceQueuePollActive = true;
441-
start = System.nanoTime();
442-
} else {
420+
while (true) {
421+
Object entry = queue.poll();
422+
if (entry == null) {
423+
if (count > 0) {
443424
assert handleContext.referenceQueuePollActive;
425+
releaseNativeObjects(context, referencesToBeFreed);
426+
handleContext.referenceQueuePollActive = false;
427+
LOGGER.fine("collected " + count + " references from native reference queue in " + ((System.nanoTime() - start) / 1000000) + "ms");
444428
}
445-
count++;
446-
LOGGER.fine(() -> PythonUtils.formatJString("releasing %s, no remaining managed references", entry));
447-
if (entry instanceof PythonObjectReference reference) {
448-
if (HandlePointerConverter.pointsToPyHandleSpace(reference.pointer)) {
449-
assert nativeStubLookupGet(handleContext, reference.pointer, reference.handleTableIndex) != null : Long.toHexString(reference.pointer);
450-
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object %x => %s", reference.pointer, reference));
451-
nativeStubLookupRemove(handleContext, reference);
429+
return manuallyCollected;
430+
}
431+
if (count == 0) {
432+
assert !handleContext.referenceQueuePollActive;
433+
handleContext.referenceQueuePollActive = true;
434+
start = System.nanoTime();
435+
} else {
436+
assert handleContext.referenceQueuePollActive;
437+
}
438+
count++;
439+
LOGGER.fine(() -> PythonUtils.formatJString("releasing %s, no remaining managed references", entry));
440+
if (entry instanceof PythonObjectReference reference) {
441+
if (HandlePointerConverter.pointsToPyHandleSpace(reference.pointer)) {
442+
assert nativeStubLookupGet(handleContext, reference.pointer, reference.handleTableIndex) != null : Long.toHexString(reference.pointer);
443+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object %x => %s", reference.pointer, reference));
444+
nativeStubLookupRemove(handleContext, reference);
445+
/*
446+
* We may only free native object stubs if their reference count is
447+
* zero. We cannot free other structs (e.g. PyDateTime_CAPI) because we
448+
* don't know if they are still used from native code. Those must be
449+
* free'd at context finalization.
450+
*/
451+
long stubPointer = HandlePointerConverter.pointerToStub(reference.pointer);
452+
long newRefCount = subNativeRefCount(stubPointer, MANAGED_REFCNT);
453+
if (newRefCount == 0) {
454+
LOGGER.finer(() -> PythonUtils.formatJString("No more references for %s (refcount->0): freeing native stub", reference));
455+
freeNativeStub(reference);
456+
} else {
457+
LOGGER.finer(() -> PythonUtils.formatJString("Some native references to %s remain (refcount=%d): not freeing native stub yet", reference, newRefCount));
452458
/*
453-
* We may only free native object stubs if their reference count is
454-
* zero. We cannot free other structs (e.g. PyDateTime_CAPI) because
455-
* we don't know if they are still used from native code. Those must
456-
* be free'd at context finalization.
459+
* In this case, the object is no longer referenced from managed but
460+
* still from native code (since the reference count is greater 0).
461+
* This case is possible if there are reference cycles that include
462+
* managed objects. We overwrite field 'CFields.GraalPyObject__id'
463+
* to avoid incorrect reuse of the ID which could resolve to another
464+
* object.
457465
*/
458-
long stubPointer = HandlePointerConverter.pointerToStub(reference.pointer);
459-
long newRefCount = subNativeRefCount(stubPointer, MANAGED_REFCNT);
460-
if (newRefCount == 0) {
461-
LOGGER.finer(() -> PythonUtils.formatJString("No more references for %s (refcount->0): freeing native stub", reference));
462-
freeNativeStub(reference);
463-
} else {
464-
LOGGER.finer(() -> PythonUtils.formatJString("Some native references to %s remain (refcount=%d): not freeing native stub yet", reference, newRefCount));
465-
/*
466-
* In this case, the object is no longer referenced from managed
467-
* but still from native code (since the reference count is
468-
* greater 0). This case is possible if there are reference
469-
* cycles that include managed objects. We overwrite field
470-
* 'CFields.GraalPyObject__id' to avoid incorrect reuse of the
471-
* ID which could resolve to another object.
472-
*/
473-
CStructAccess.WriteIntNode.writeUncached(stubPointer, CFields.GraalPyObject__handle_table_index, 0);
474-
// this can only happen if the object is a GC object
475-
assert reference.gc;
476-
/*
477-
* Since the managed object is already dead (only the native
478-
* object stub is still alive), we need to remove the object
479-
* from its current GC list. Otherwise, the Python GC would try
480-
* to traverse the object on the next collection which would
481-
* lead to a crash.
482-
*/
483-
GCListRemoveNode.executeUncached(stubPointer);
484-
}
485-
} else {
486-
assert nativeLookupGet(handleContext, reference.pointer) != null : Long.toHexString(reference.pointer);
487-
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object with replacement %x => %s", reference.pointer, reference));
488-
nativeLookupRemove(handleContext, reference.pointer);
489-
if (reference.isFreeAtCollection()) {
490-
LOGGER.finer(() -> PythonUtils.formatJString("freeing managed object %s replacement", reference));
491-
freeNativeStruct(reference);
492-
}
466+
CStructAccess.WriteIntNode.writeUncached(stubPointer, CFields.GraalPyObject__handle_table_index, 0);
467+
// this can only happen if the object is a GC object
468+
assert reference.gc;
469+
/*
470+
* Since the managed object is already dead (only the native object
471+
* stub is still alive), we need to remove the object from its
472+
* current GC list. Otherwise, the Python GC would try to traverse
473+
* the object on the next collection which would lead to a crash.
474+
*/
475+
GCListRemoveNode.executeUncached(stubPointer);
493476
}
494-
} else if (entry instanceof NativeObjectReference reference) {
495-
LOGGER.finer(() -> PythonUtils.formatJString("releasing native lookup for native object %x => %s", reference.pointer, reference));
477+
} else {
478+
assert nativeLookupGet(handleContext, reference.pointer) != null : Long.toHexString(reference.pointer);
479+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native stub lookup for managed object with replacement %x => %s", reference.pointer, reference));
496480
nativeLookupRemove(handleContext, reference.pointer);
497-
processNativeObjectReference(reference, referencesToBeFreed);
498-
} else if (entry instanceof NativeStorageReference reference) {
499-
handleContext.nativeStorageReferences.remove(reference);
500-
processNativeStorageReference(reference);
501-
} else if (entry instanceof PyCapsuleReference reference) {
502-
handleContext.pyCapsuleReferences.remove(reference);
503-
processPyCapsuleReference(reference);
481+
if (reference.isFreeAtCollection()) {
482+
LOGGER.finer(() -> PythonUtils.formatJString("freeing managed object %s replacement", reference));
483+
freeNativeStruct(reference);
484+
}
504485
}
505-
}
506-
} finally {
507-
CExtCommonNodes.ReadAndClearNativeException.executeUncached(threadState);
508-
if (savedException != PNone.NO_VALUE) {
509-
CExtCommonNodes.TransformExceptionToNativeNode.executeUncached(savedException);
486+
} else if (entry instanceof NativeObjectReference reference) {
487+
LOGGER.finer(() -> PythonUtils.formatJString("releasing native lookup for native object %x => %s", reference.pointer, reference));
488+
nativeLookupRemove(handleContext, reference.pointer);
489+
processNativeObjectReference(reference, referencesToBeFreed);
490+
} else if (entry instanceof NativeStorageReference reference) {
491+
handleContext.nativeStorageReferences.remove(reference);
492+
processNativeStorageReference(reference);
493+
} else if (entry instanceof PyCapsuleReference reference) {
494+
handleContext.pyCapsuleReferences.remove(reference);
495+
processPyCapsuleReference(reference);
510496
}
511497
}
512498
}
@@ -559,19 +545,32 @@ private static void processPyCapsuleReference(PyCapsuleReference reference) {
559545
* element. This method may therefore run arbitrary guest code and strictly requires the GIL to
560546
* be held at the time of invocation.
561547
*/
562-
private static void releaseNativeObjects(NativeObjectReferenceArrayWrapper referencesToBeFreed) {
548+
private static void releaseNativeObjects(PythonContext context, NativeObjectReferenceArrayWrapper referencesToBeFreed) {
563549
if (!referencesToBeFreed.isEmpty()) {
564550
/*
565551
* This needs the GIL because this will call the native objects' destructors which can
566552
* be arbitrary guest code.
567553
*/
568554
assert PythonContext.get(null).ownsGil();
569-
LOGGER.fine(() -> PythonUtils.formatJString("releasing %d NativeObjectReference instances", referencesToBeFreed.getArraySize()));
570-
Object array = AllocateNode.allocUncached(referencesToBeFreed.getArraySize() * Long.BYTES);
571-
CStructAccess.WriteLongNode.getUncached().writeLongArray(array, referencesToBeFreed.getArray(), (int) referencesToBeFreed.getArraySize(), 0, 0);
572-
PCallCapiFunction.callUncached(NativeCAPISymbol.FUN_BULK_DEALLOC, array, referencesToBeFreed.getArraySize());
573-
FreeNode.executeUncached(array);
574-
referencesToBeFreed.reset();
555+
PythonContext.PythonThreadState threadState = context.getThreadState(context.getLanguage());
556+
/*
557+
* There can be an active exception. Since we might be calling arbitary python, we need
558+
* to stash it.
559+
*/
560+
Object savedException = CExtCommonNodes.ReadAndClearNativeException.executeUncached(threadState);
561+
try {
562+
LOGGER.fine(() -> PythonUtils.formatJString("releasing %d NativeObjectReference instances", referencesToBeFreed.getArraySize()));
563+
Object array = AllocateNode.allocUncached(referencesToBeFreed.getArraySize() * Long.BYTES);
564+
CStructAccess.WriteLongNode.getUncached().writeLongArray(array, referencesToBeFreed.getArray(), (int) referencesToBeFreed.getArraySize(), 0, 0);
565+
PCallCapiFunction.callUncached(NativeCAPISymbol.FUN_BULK_DEALLOC, array, referencesToBeFreed.getArraySize());
566+
FreeNode.executeUncached(array);
567+
referencesToBeFreed.reset();
568+
} finally {
569+
CExtCommonNodes.ReadAndClearNativeException.executeUncached(threadState);
570+
if (savedException != PNone.NO_VALUE) {
571+
CExtCommonNodes.TransformExceptionToNativeNode.executeUncached(savedException);
572+
}
573+
}
575574
}
576575
}
577576

0 commit comments

Comments
 (0)