在 Python ctypes 回调中发生异常时避免未定义的行为?

Avoiding undefined behavior when an exception occurs in a Python ctypes callback?

这个 C 共享库 test.so 定义了 cfunc(),它接受一个回调函数:

int cfunc(int (*callback)(void)) {
  return callback();
}

此 Python 代码调用 cfunc() 使用 Python 函数 callback():

from ctypes import *

lib = cdll.LoadLibrary('./test.so')

@CFUNCTYPE(c_int)
def callback():
  raise RuntimeError

print(lib.cfunc(callback))

因为 callback() 引发异常,所以它永远不会 return 成为一个值。我相信这会导致 ctypes 触发未定义的行为,尽管我还没有仔细研究源代码来确定。至少,由 cfunc() 编辑的值 return 似乎表明它未初始化。

有什么办法可以避免这种未定义的行为吗?

callback() 的名义主体包裹在 try 子句中会在 except 子句中留下例外的可能性。

我可以将 int * 传递给 callback() 并使用引用的 int 来表示是否引发了异常。但是,我认为未定义的行为仍然发生在我无法控制的代码中。

有没有办法让ctypesCFunctionType对象初始化return值? ctypes 文档似乎没有提及任何内容。

有多种可能性,一种是将纯 python 函数包装到 try..except 中,然后才使用 ctypes:[=29= jit C 函数]

import ctypes
def SafeCFunctype(py_fun):
    @ctypes.CFUNCTYPE(ctypes.c_int)
    def safe():
        try:
            return py_fun()
        except:
            print("Exception!")
            return -1
    return safe

为简单起见,我选择 return -1 并记录到标准输出,以防发生错误,但还有其他选项可以指示错误。

现在:

@SafeCFunctype
def callback1():
    raise RuntimeError()
    
callback1() # results in
# Exception!
# -1

然而,py_fun 的代码并不是唯一可能发生错误的地方 - 当 python 对象转换为 c- 时,胶水代码中也可能出现异常整数,例如:

@SafeCFunctype
def callback2():
    return None

callback2()
# Exception ignored in: <function SafeCFunctype.<locals>.safe at 0x7f31f41dd840>
# -97323988

为确保转换有效(或异常发生在 try..catch 内部,我们可以执行以下操作:

import ctypes
def SafeCFunctype(py_fun):
    @ctypes.CFUNCTYPE(ctypes.c_int)
    def safe():
        try:
            # throws if conversion not possible:
            return ctypes.c_int(py_fun())
        except:
            print("Exception!")
            return -1
    return safe

现在转换(和可能的异常)发生在 ctypes.c_int(py_fun()) 内部,我们可以肯定,创建的 CFunction 中的样板代码不会出错:

@SafeCFunctype
def callback2():
    return None
    
callback2() # results in
# Exception!
# -1

即没有随机结果。


对于像 return -1 这样的简单代码,我们可以非常确定,不会引发进一步的异常 except 子句。

我没有看到任何方法来检测包装回调中是否引发了 python 异常,因为 ctypes 会检测到引发了异常,打印警告然后清除错误。

这是上面的例子:我使用 ffi 来 jit 一个 C-wrapper,它将调用 ctypes 提供的 callback 并检查 Python-错误:

import cffi

code_template = r"""
int safe_call(void){{
   typedef int (*functor)(void);
   // this is awfull, but that is how it works:
   functor fun = *((functor *){callback_addressof});
   int res = fun();
   if(PyErr_Occurred()){{
      PyErr_Clear();
      printf("Error detected\n");
      return -1;
   }}
   else{{
      printf("No error detected\n");
      return res;
   }}
}}
"""

def create_safe_functor(functor):
    ffibuilder = cffi.FFI()
    ffibuilder.cdef("int safe_call(void);", override=True)
    code = code_template.format(callback_addressof=ctypes.addressof(functor))
    print(code)
    built_module=ffibuilder.verify(source=code)
    safe_functor = built_module.safe_call
    return safe_functor

现在

fun = create_safe_functor(callback)
fun()
# RuntimeError:
# No error detected
# -234235

这意味着 C 函数不再能够检测到错误。