Description
Main Issue
documentation for ctypes.PYFUNCTYPE in cytpes is misleading
The returned function prototype creates functions that use the Python calling convention. The function will not release the GIL during the call.
Proposal
I think it should be updated to something else, indicating that the GIL is acquired and used as normal, rather than held until the call is complete -
The returned function prototype creates functions that use the Python calling convention. The function will acquire the GIL and use it like normal python code during the call.
What I was trying
I was trying to create a function that holds the python GIL for x seconds without releasing it. I wanted to use this to test some functions that should release the GIL and still allow the GIL to be acquired by another function.
the documentation for ctypes.PYFUNCTYPE
mislead me into thinking wrapping a time.sleep
call with this would hold the GIL- (The function will not release the GIL during the call
)
How I tested it
1. create an example.c file
// example.c
#include <stdio.h>
#include <Python.h>
void run_in_c(int value, void (*callback)(int)) {
printf("C function called with %d\n", value);
callback(value);
}
// compile with
// export C_INCLUDE_PATH="/usr/local/Cellar//python@3.11/3.11.7/Frameworks/Python.framework/Versions/3.11/include/python3.11"
// gcc -shared -o example.so -fPIC example.c
2. compile to create example.so
export C_INCLUDE_PATH="/usr/local/Cellar//python@3.11/3.11.7/Frameworks/Python.framework/Versions/3.11/include/python3.11"
gcc -shared -o example.so -fPIC example.c
3. run a python file to see if we created a function that does not release the GIL
import ctypes
from concurrent.futures import ThreadPoolExecutor
import time
example_lib = ctypes.CDLL('example.so')
def newfn(x):
time.sleep(x)
print("ran in python", x)
should_block_gil = ctypes.PYFUNCTYPE(None, ctypes.c_int)(newfn)
def blocking_sleep(x):
example_lib.run_in_c(x, should_block_gil)
start = time.perf_counter()
results = ThreadPoolExecutor(2).map(blocking_sleep, [1]*2)
blocking_sleep(1)
print(list(results))
print(time.perf_counter() - start)
The output was
C function called with 1
C function called with 1
C function called with 1
ran in python 1
ran in python 1
ran in python 1
[None, None]
1.002278681
clearly indicating that the GIL was released by time.sleep