likes
comments
collection
share

linux下python ctypes库实现原理(1)- 动态链接库的加载、关闭以及函数获取

作者站长头像
站长
· 阅读数 63

ctypes库

ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。

下面我们以linux系统、python3.8为例,来研究一下它的内部原理

linux中 ctypes库基本工作原理

c语言中动链接库加载、函数地址获取

  • 使用dlfcn系列相关函数实现

我们以下面的简单c语言代码为例,先捋一下用c语言实现加载动态链接库并调用其中函数的流程,然后再从python源码的角度看下ctypes的实现原理.

提供库函数的代码

#include<stdio.h>

int max(int a, int b) {
    if (a >= b) {
        return a;
    } else {
        return b;
    }
}

使用gcc -Wall -g -fPIC -shared -o test.so.0 test_so.c编译成动态链接库.so

实测代码

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef int (*max_func)(int, int);

int main() {
    // 加载动态链接库,获取文件句柄handler
    void *handle = dlopen("./test.so.0", RTLD_NOW);
    if (handle == NULL) {
        fprintf(stderr, "%s\n", dlerror());
        printf("open failed!!!\n");
        exit(-1);
    } else {
        printf("open success!!!\n");
    }
    // 通过句柄获取某个函数
    max_func max = dlsym(handle, "max");
    if (max == NULL) {
        printf("find func failed!!!\n");
        exit(-1);
    } else {
        printf("find func success!!!\n");
    }
    int a = 8;
    int b = 9;
    // 调用动态链接库中的函数
    printf("max is %d\n", max(a, b));
    // 关闭动态链接库
    dlclose(handle);
    return 0;
}

使用gcc test_dlopen.c -ldl编译成二进制文件,./a.out执行结果:

open success!!!
find func success!!!
max is 9

ctypes库对于整个流程的封装

python代码实现

首先,用python代码实现上述流程

from ctypes import cdll
from _ctypes import dlclose
# 加载动态链接库
library = cdll.LoadLibrary("./test.so.0")
a = 8
b = 9
# 调用库中的max函数
print(f"max is: {library.max(a, b)}")
# 关闭动态链接库
dlclose(library._handle)

python实现原理

动态链接库加载

我们追一下python代码干了什么(只列出关键代码,不关键的代码用...代替)

from _ctypes import dlopen as _dlopen
# cdll是什么
cdll = LibraryLoader(CDLL)

class LibraryLoader(object):
...
    def LoadLibrary(self, name):
        return self._dlltype(name)

# 所以,实际上调用cdll.LoadLibrary方法,其实是实例化了一个CDLL类的对象

class CDLL(object):
...

    def __init__(self, name, mode=DEFAULT_MODE, handle=None,
                 use_errno=False,
                 use_last_error=False,
                 winmode=None):
    ...
    # 实际上,是调用了_ctypes模块的dlopen方法,打开了动态链接库
    if handle is None:
        self._handle = _dlopen(self._name, mode)
    else:
        self._handle = handle

获取动态链接库中的函数

from _ctypes import CFuncPtr as _CFuncPtr
class CDLL(object):
    _func_flags_ = _FUNCFLAG_CDECL
    _func_restype_ = c_int
...
    def __init__(self, name, mode=DEFAULT_MODE, handle=None,
                 use_errno=False,
                 use_last_error=False,
                 winmode=None):
        
        // 函数包装类,默认restype是c_int
        class _FuncPtr(_CFuncPtr):
            _flags_ = flags
            _restype_ = self._func_restype_
        self._FuncPtr = _FuncPtr

    def __getattr__(self, name):
        if name.startswith('__') and name.endswith('__'):
            raise AttributeError(name)
        func = self.__getitem__(name)
        setattr(self, name, func)
        return func

    def __getitem__(self, name_or_ordinal):
        func = self._FuncPtr((name_or_ordinal, self))
        if not isinstance(name_or_ordinal, int):
            func.__name__ = name_or_ordinal
        return func

python中只能看到获取方法应该是实例化了一个CFuncPtr类的对象,该类实际上继承自_CData,在c中实现,我们继续深追CFuncPtr类的实例化流程以及call流程(因为library.max(a, b),实际上library.max是一个CFuncPtr对象,对象加括号调用的是对象的tp_call方法)

static int
_ctypes_add_types(PyObject *mod)
{
    ...
    MOD_ADD_TYPE(&PyCFuncPtr_Type, &PyCFuncPtrType_Type, &PyCData_Type);
}

PyTypeObject PyCFuncPtr_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    ...
    (ternaryfunc)PyCFuncPtr_call,               /* tp_call */
    ...
    PyCFuncPtr_new,                             /* tp_new */
}

static PyObject *
PyCFuncPtr_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    ...
    // func = self._FuncPtr((name_or_ordinal, self)), 所以参数个数应该是1
    if (1 <= PyTuple_GET_SIZE(args) && PyTuple_Check(PyTuple_GET_ITEM(args, 0)))
        return PyCFuncPtr_FromDll(type, args, kwds);
}

static PyObject *
PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    const char *name;
    int (* address)(void);
    PyObject *ftuple;
    PyObject *dll;
    PyObject *obj;
    PyCFuncPtrObject *self;
    void *handle;
    PyObject *paramflags = NULL;
    // 解析参数,这个时候ftuple是(name_or_ordinal, self)
    if (!PyArg_ParseTuple(args, "O|O", &ftuple, &paramflags))
        return NULL;
    // 解析元组,拿到cdll对象
    if (!PyArg_ParseTuple(ftuple, "O&O;illegal func_spec argument",
                          _get_name, &name, &dll))
    {
        Py_DECREF(ftuple);
        return NULL;
    }
    ...
    // 获取动态链接库的handle
    obj = PyObject_GetAttrString(dll, "_handle");
    // int对象值转为指针
    handle = (void *)PyLong_AsVoidPtr(obj);
    // 通过dlsym获取函数地址
    address = (PPROC)ctypes_dlsym(handle, name);
    // 函数地址赋值给b_ptr
    *(void **)self->b_ptr = address;
    // 设置callable是self
    self->callable = (PyObject *)self;
}

动态链接库关闭

ctypes并没有提供相关close动态链接库函数,但是从加载动态链接库的源码中,我们得知其实是调用了_dlopen方法,把handle赋值给了self._hanle,因此我们直接调用dlclose(library._handle)来实现

dlclose(library._handle)

python实现原理

我们接着往下深追一下_ctypes模块针对整个流程的实现:

// 相关的宏定义
#define ctypes_dlsym dlsym
#define ctypes_dlerror dlerror
#define ctypes_dlopen dlopen
#define ctypes_dlclose dlclose
#define ctypes_dladdr dladdr
// 函数定义表
PyMethodDef _ctypes_module_methods[] = {
...
    {"dlopen", py_dl_open, METH_VARARGS,
     "dlopen(name, flag={RTLD_GLOBAL|RTLD_LOCAL}) open a shared library"},
    {"dlclose", py_dl_close, METH_VARARGS, "dlclose a library"},
    {"dlsym", py_dl_sym, METH_VARARGS, "find symbol in shared library"},
...
}

static PyObject *py_dl_open(PyObject *self, PyObject *args)
{
    PyObject *name, *name2;
    const char *name_str;
    void * handle;
    ...
    // 解析python传下来的so名字
    if (!PyArg_ParseTuple(args, "O|i:dlopen", &name, &mode))
        return NULL;
    // 模式
    mode |= RTLD_NOW;
    ...
    // 根据上面的宏定义分析,那么这里ctypes_dlopen其实就是dlopen
    handle = ctypes_dlopen(name_str, mode);
    ...
    // 将指针转换成int对象返回给python,因此从python代码中拿到的返回值(int对象),就是handle指针的值
    return PyLong_FromVoidPtr(handle);
}

static PyObject *py_dl_close(PyObject *self, PyObject *args)
{
    void *handle;
    // 解析参数
    if (!PyArg_ParseTuple(args, "O&:dlclose", &_parse_voidp, &handle))
        return NULL;
    // 关闭动态链接库
    if (dlclose(handle)) {
        PyErr_SetString(PyExc_OSError,
                               ctypes_dlerror());
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyObject *py_dl_sym(PyObject *self, PyObject *args)
{
    char *name;
    void *handle;
    void *ptr;

    // 解析参数
    if (!PyArg_ParseTuple(args, "O&s:dlsym",
                          &_parse_voidp, &handle, &name))
        return NULL;
    ...
    // 获取函数
    ptr = ctypes_dlsym((void*)handle, name);
    if (!ptr) {
        PyErr_SetString(PyExc_OSError,
                               ctypes_dlerror());
        return NULL;
    }
    // 将函数地址将转换成int对象返回给python,因此从python代码中拿到的返回值(int对象),就是方法地址
    return PyLong_FromVoidPtr(ptr);
}
转载自:https://juejin.cn/post/7375532797383147529
评论
请登录