如何使用Python中的ctypes重载C库中弱声明的函数?

3
我正在尝试建立一个测试环境来验证我正在开发的C库。该库嵌入在运行自定义Linux的设备上。在网上阅读了一些资料后,我决定使用Python和ctypes从Python调用我的库函数。对于几乎所有的函数都能正常工作,但是当涉及到回调函数时,我遇到了困难。
我的回调函数在库中被定义为"weak"。我想知道是否可以使用Python ctypes重载我的弱C函数?
以下是我尝试过的示例代码: libtest.c
#include <stdio.h>
#include <stdlib.h>
#include "libtest.h"

int multiply(int a, int b)
{
    custom_callback(a);
    return a*b;
}

int __attribute__((weak)) custom_callback(int a)
{
    printf("Callback not redefined, a = %d\n", a);
    return 0;
}

main.py

from ctypes import *

lib = cdll.LoadLibrary("libtest.so")

CALLBACKFUNC = CFUNCTYPE(c_int, c_int)

def py_callback_func(a):
    print("py redifinition", a)
    return 0


lib.custom_callback = CALLBACKFUNC(py_callback_func)

lib.multiply(c_int(5), c_int(10))

然后我使用Python3运行我的main.py文件:

$ python3 main.py 
Callback not redefined, a = 5

我期望的输出结果是py redifinition 5 我做错了什么吗?或者说 ctypes 简单地无法重新定义声明为 weak 的 C 函数吗?

弱链接由动态链接器进行调节。据我所知,在实例化Python库的包装器后,您对其进行的任何操作都不会以任何方式调制动态链接。 - John Bollinger
1个回答

2
你有些混淆了:符号是在链接时处理的,而你正在运行时“操作”。链接器不可能知道你的Python回调函数。虽然这不是官方来源,请查看[维基百科]: 弱符号获取更多详细信息。
此外,[Python.Docs]: ctypes - 用于Python的外部函数库
这里有一个小演示。

libtest00.c:

#include <stdio.h>


int __attribute__((weak)) custom_callback(int a) {
    printf("Callback not redefined, a = %d\n", a);
    return 0;
}


int multiply(int a, int b) {
    custom_callback(a);
    return a * b;
}

callback00.c:

#include <stdio.h>


int custom_callback(int a) {
    printf("C callback redefinition, a = %d\n", a);
    return 0;
}

code00.py:

#!/usr/bin/env python

import sys
import ctypes as ct


DLL_NAME = "./libtest000.so"
CALLBACKFUNCTYPE = ct.CFUNCTYPE(ct.c_int, ct.c_int)


def py_callback_func(a):
    print("PY callback redefinition", a)
    return 0


def main(*argv):
    dll_name = argv[0] if argv else DLL_NAME
    dll = ct.CDLL(dll_name)
    multiply = dll.multiply
    multiply.argtypes = [ct.c_int, ct.c_int]
    multiply.restype = ct.c_int
    #dll.custom_callback(3)
    #dll.custom_callback = CALLBACKFUNCTYPE(py_callback_func)
    #dll.custom_callback(3)
    res = multiply(5, 10)
    print("{:} returned {:d}".format(multiply.__name__, res))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

Output:

[cfati@cfati-5510-0:/mnt/e/Work/Dev/StackOverflow/q055357490]> ~/sopr.sh
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[064bit prompt]> ls
callback00.c  code00.py  code01.py  libtest00.c  libtest01.c
[064bit prompt]> gcc -o libtest000.so -fPIC -shared libtest00.c
[064bit prompt]> gcc -o libtest001.so -fPIC -shared libtest00.c callback00.c
[064bit prompt]> ls
callback00.c  code00.py  code01.py  libtest00.c  libtest000.so  libtest001.so  libtest01.c
[064bit prompt]>
[064bit prompt]> python code00.py ./libtest000.so
Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] 64bit on linux

Callback not redefined, a = 5
multiply returned 50

Done.
[064bit prompt]>
[064bit prompt]> python code00.py ./libtest001.so
Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] 64bit on linux

C callback redefinition, a = 5
multiply returned 50

Done.
如上所述,__attribute__((weak))的效果是可见的。此外,关于已注释的代码(试图设置.dll函数):它只在CTypes代理级别操作,不会改变.dll代码(这将是荒谬的)。
至于你的问题,有几种解决方法。以下是其中一种使用指针持有此类回调函数的方法。该指针可以由一个setter函数(也被导出)从外部设置。 libtest01.c:
#include <stdio.h>

#define EXEC_CALLBACK(X) \
    if (pCallback) { \
        pCallback(X); \
    } else { \
        fallbackCallback(X); \
    }


typedef int (*CustomCallbackPtr)(int a);

static CustomCallbackPtr pCallback = NULL;


static int fallbackCallback(int a) {
    printf("Callback not redefined, a = %d\n", a);
    return 0;
}


int multiply(int a, int b) {
    EXEC_CALLBACK(a);
    return a * b;
}


void setCallback(CustomCallbackPtr ptr) {
    pCallback = ptr;
}

code01.py:

#!/usr/bin/env python

import sys
import ctypes as ct


DLL_NAME = "./libtest01.so"
CALLBACKFUNCTYPE = ct.CFUNCTYPE(ct.c_int, ct.c_int)


def py_callback_func(a):
    print("PY callback redefinition", a)
    return 0


def main(*argv):
    dll = ct.CDLL(DLL_NAME)
    multiply = dll.multiply
    multiply.argtypes = [ct.c_int, ct.c_int]
    multiply.restype = ct.c_int
    set_callback = dll.setCallback
    set_callback.argtypes = [CALLBACKFUNCTYPE]
    multiply(5, 10)
    set_callback(CALLBACKFUNCTYPE(py_callback_func))
    multiply(5, 10)
    set_callback(ct.cast(ct.c_void_p(), CALLBACKFUNCTYPE))
    multiply(5, 10)


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

Output:

[064bit prompt]> ls  # Files from previous step
callback00.c  code00.py  code01.py  libtest00.c  libtest000.so  libtest001.so  libtest01.c
[064bit prompt]> gcc -o libtest01.so -fPIC -shared libtest01.c
[064bit prompt]>
[064bit prompt]> ls
callback00.c  code00.py  code01.py  libtest00.c  libtest000.so  libtest001.so  libtest01.c  libtest01.so
[064bit prompt]>
[064bit prompt]> python code01.py
Python 3.8.5 (default, Jan 27 2021, 15:41:15) [GCC 9.3.0] 64bit on linux

Callback not redefined, a = 5
PY callback redefinition 5
Callback not redefined, a = 5

Done.

感谢CristiFati提供详细的文档回答。我认为我将从库中删除方便的“弱”属性,因为它可能会在未来引起可移植性问题,并且不太适合ctypes Python测试。使用函数指针和setter似乎是更清晰的解决方案。 - Thomas B
是的,这将最适合您的情况。__attribute__((weak))是一个不错且强大的功能(由一些编译器支持),但在这里并不太适用。 - CristiFati

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接