linux下python ctypes库实现原理(1)- 动态链接库的加载、关闭以及函数获取
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, ¶mflags))
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