一. 概述
项目中使用Python总会由于某种原因(人力/时间/金钱等等),需要借助外部代码,幸好Python是容易扩展的语言,本文浅析三种常见扩展方式:subprocess调用外部可执行程序;ctypes调用外部DLL函数;C/C++ Extending标准库。用一个需求的三种实现方式贯穿始终,正是对应标题的“Python调用外部代码的三种方式”,该需求比“hello,world”复杂,但依然只有百行规模的代码,容易看懂,力争做好“万事开头难”中的“开头”,以后修行就看个人了,同时作为自己学习的备忘,以免隔三差五查文档或犯同样的错误。理解全文需熟悉Python和C,如果有Linux/UNIX系统编程知识更好,全文篇幅较长,不得不分三篇(上,中,下)发布,上篇包括subprocess和ctypes,中篇是C/C++ Extending,下篇包含文中所有代码压缩包的附件。
此处“外部代码”是指:
1. 本地可执行程序,通常是原生二进制文件或解释器文本文件。
2. 本地动态链接库,通常由C/C++编译而成。
先简要概括下这三种调用方式:
1. subprocess。这是一个标准库,可以运行外部程序,接口简单易用,容易跨平台。不过灵活性欠佳,因为Python无法控制外部程序的运行逻辑,只适合做简单的输入输出交互。
2. ctypes。这是一个标准库,可以加载动态链接库,借助ctypes提供的wrapper,可以方便的使用C库函数和系统调用,功能和灵活性大增,做hack的利器。不过毕竟是按照C API的方式利用这类接口,用动态解释语言去适配静态编译语言接口,别扭程度可想而知。
3. extending。这是一种实现方式,全称是Python C/C++ Extending,即用C/C++写Python模块或定制Python解释器,这是扩展Python的终极方案,可以随心所欲,将项目已有的C/C++接口包装为Python模块,或将有性能瓶颈的纯Python模块用C/C++重写,或为Python增加内置函数和对象。不过这是扩展Python中最复杂的方式,将模块与CPython绑定,而与JPython/Iron Python/PyPy无缘了,关键是“来到危险的C/C++世界”!
二. 系统要求
关于测试环境。本人的系统是Fedora 20 (Linux 3.13, gcc 4.8, Python 3.3),如果kernel/gcc/Python的版本低于我的测试环境,示例代码可能要微调。
关于C++。后文中所有出现“C”的地方,都可以用“C++”代替,实现原理不变,但本文的实现用C语法,如果要移植到C++,需要做几点语法上的调整,留给感兴趣的读者。
关于示例代码。如果先前没有接触过本文介绍的三种方式,快速进入状态的方法就是下载附件中的代码,编译运行调试修改,同时看文档找资料,示例虽简单,但属于麻雀虽小五脏俱全的类型,有助于在这三个领域中继续深入。
三. 需求描述
简单说就是监控一个目录内文件的变化(访问,打开,关闭,数据修改,属性修改,移动,删除等等),然后打印出(时间,文件名,相关事件)。方案也是现成的,直接用Linux的inotify机制,比如BSD的kqueue也提供了类似功能,但Python标准库没有inotify API,这也正好是Python需调用外部代码的场景之一。
四. subprocess调用外部可执行程序
场景。需求已由外部程序实现,Python只需要做简单输入输出的整合。
实现。
1. 准备C程序,作为外部代码。
-
#include <limits.h>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <string.h>
-
#include <sys/inotify.h>
-
#include <sys/time.h>
-
#include <time.h>
-
#include <unistd.h>
-
-
#define EVENT_SIZE_MAX (sizeof(struct inotify_event) + NAME_MAX + 1)
-
#define EVENT_SIZE_MIN (sizeof(struct inotify_event))
-
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
-
-
typedef struct {
-
char *name;
-
uint32_t value;
-
} mask_event;
-
-
static const mask_event MASK_EVENTS[] = {
-
{"IN_ACCESS", IN_ACCESS},
-
{"IN_ATTRIB", IN_ATTRIB},
-
{"IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE},
-
{"IN_CLOSE_WRITE", IN_CLOSE_WRITE},
-
{"IN_CREATE", IN_CREATE},
-
{"IN_DELETE", IN_DELETE},
-
{"IN_DELETE_SELF", IN_DELETE_SELF},
-
{"IN_DONT_FOLLOW", IN_DONT_FOLLOW},
-
{"IN_EXCL_UNLINK", IN_EXCL_UNLINK},
-
{"IN_IGNORED", IN_IGNORED},
-
{"IN_ISDIR", IN_ISDIR},
-
{"IN_MASK_ADD", IN_MASK_ADD},
-
{"IN_MODIFY", IN_MODIFY},
-
{"IN_MOVED_FROM", IN_MOVED_FROM},
-
{"IN_MOVED_TO", IN_MOVED_TO},
-
{"IN_MOVE_SELF", IN_MOVE_SELF},
-
{"IN_ONESHOT", IN_ONESHOT},
-
{"IN_ONLYDIR", IN_ONLYDIR},
-
{"IN_OPEN", IN_OPEN},
-
{"IN_Q_OVERFLOW", IN_Q_OVERFLOW},
-
{"IN_UNMOUNT", IN_UNMOUNT}
-
};
-
-
void print_event(const char *target, const struct inotify_event *ev);
-
-
int main(int argc, char *argv[])
-
{
-
/* at least one event available in the buffer */
-
char ev_buffer[EVENT_SIZE_MAX];
-
ssize_t len, offset;
-
struct inotify_event *p = NULL;
-
const char *pathname = NULL;
-
int fd, wd;
-
-
if (argc != 2) {
-
fprintf(stderr, "usage: notify path\n");
-
exit(EXIT_FAILURE);
-
}
-
pathname = argv[1];
-
-
fd = inotify_init();
-
if (fd == -1) {
-
perror("inotify_init");
-
exit(EXIT_FAILURE);
-
}
-
wd = inotify_add_watch(fd, pathname, IN_ALL_EVENTS);
-
if (wd == -1) {
-
perror("inotify_add_watch");
-
exit(EXIT_FAILURE);
-
}
-
-
while (1) {
-
len = read(fd, ev_buffer, sizeof(ev_buffer));
-
if (len < (ssize_t)EVENT_SIZE_MIN) {
-
perror("read");
-
break;
-
}
-
-
offset = 0;
-
while (offset < len) {
-
p = (struct inotify_event*)(ev_buffer + offset);
-
print_event(pathname, p);
-
offset += sizeof(*p) + p->len;
-
}
-
}
-
inotify_rm_watch(fd, wd);
-
close(fd);
-
-
return 0;
-
}
-
-
void print_event(const char *target, const struct inotify_event *ev)
-
{
-
struct timeval tv;
-
struct tm *lt;
-
const char *name = ev->len > 1 ? ev->name : target;
-
size_t idx;
-
const mask_event *me;
-
-
gettimeofday(&tv, NULL);
-
lt = localtime(&tv.tv_sec);
-
printf("%02d:%02d:%02d,%03ld file(%s) ",
-
lt->tm_hour, lt->tm_min, lt->tm_sec, tv.tv_usec/1000, name);
-
for (idx = 0; idx < ARRAY_SIZE(MASK_EVENTS); ++idx) {
-
me = &MASK_EVENTS[idx];
-
if (ev->mask & me->value) printf("%s ", me->name);
-
}
-
printf("\n");
-
}
如果对Linux inotify API不熟悉,可以man inotify或查看在线文档。
代码行72~73:struct inotify_event表示的真实内容是变长的,但受限于系统文件名长度,可以知道最大长度
EVENT_SIZE_MAX,传入的ev_buffer至少保证放入一个事件,否则在新内核(kernel 2.6.21以后)下会报错,如果有新事件,返回的len至少应该是EVENT_SIZE_MIN。
代码行85~86:这两句在测试中是没执行过的,通过终端手动CTRL+C,SIGINT直接终止进程了,系统可以保证清理进程打开的fd,此处仅为展示应有逻辑,但在一些实际要求可重入的环境下,必须考虑合理的清理操作。
代码行103~106:同一个ev下可能含有多个mask位,需要循环处理。
2. subprocess调用。主要工作被外部代码做了,Python只是简单传参和输出就OK,在交互shell演示结果。
编译上述C代码:
-
bash-4.2 $make notify1
-
gcc -g -O2 -Wall notify1.c -o notify1
Python shell中直接调用,监控当前目录的变化,可以看到setup.py的重命名操作:
-
(py3) bash-4.2 $python
Python 3.3.2 (default, Feb 11 2014, 10:35:02)
[GCC 4.8.2 20131212 (Red Hat 4.8.2-7)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> subprocess.check_call(['./notify1', '.'])
13:29:57,856 file(setup.py) IN_MOVED_FROM
13:29:57,856 file(setup.py.bak) IN_MOVED_TO
......
13:30:04,402 file(.) IN_CLOSE_NOWRITE IN_ISDIR
13:30:04,403 file(.) IN_ISDIR IN_OPEN
13:30:04,403 file(.) IN_CLOSE_NOWRITE IN_ISDIR
......
13:30:06,555 file(.) IN_CLOSE_NOWRITE IN_ISDIR
13:30:08,600 file(setup.py.bak) IN_MOVED_FROM
13:30:08,600 file(setup.py) IN_MOVED_TO
备忘:
*** 参数“shell=True”表示调用系统shell执行命令, 如果执行内容不可控,会有重大安全隐患,避免使用。
*** pipe相关的阻塞。实践中可能会用到更复杂的交互,比如与子进程的stdin/stdout/stderr交互,此时要注意可能的死锁,下面是一个简单例子
-
from subprocess import Popen, PIPE
-
import itertools
-
import sys
-
-
def main():
-
if len(sys.argv) != 2:
-
sys.exit('usage: {} cnt'.format(sys.argv[0]))
-
cnt = int(sys.argv[1])
-
-
p = Popen('cat', shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)
-
for e in itertools.repeat(b'x' * 1023 + b'\n', cnt):
-
p.stdin.write(e)
-
p.stdin.close()
-
print(len([e for e in p.stdout]))
-
-
if __name__ == '__main__':
-
main()
程序逻辑:每次向cat的stdin写1k数据,若干次后,在从cat的stdout读取回来。如果cnt只是几十次看不出什么问题,几百次后就会发生死锁。
问题原因:内核为pipe分配的空间是有限的,目前一个pipe为64k,双向128k,加上一些用户空间缓存,处理大量数据时很容易爆掉。
解决方案:一种办法就是用另外一个进程或线程处理stdout,工作流就像shell的pipe,连续不断,不会卡壳。
五. ctypes调用外部DLL函数
场景。无可用Python模块,只好求助于C API,搞定燃眉之急。
实现。
1. 已有第四节subprocess C版本的实现,而ctypes解决问题的途径就是调用C函数,下面的Python实现就水到渠成了。
-
from ctypes import CDLL, create_string_buffer
-
import datetime
-
import os
-
import struct
-
import sys
-
-
MASK_EVENTS = [
-
("IN_ACCESS", 0x00000001),
-
("IN_ATTRIB", 0x00000004),
-
("IN_CLOSE_NOWRITE", 0x00000010),
-
("IN_CLOSE_WRITE", 0x00000008),
-
("IN_CREATE", 0x00000100),
-
("IN_DELETE", 0x00000200),
-
("IN_DELETE_SELF", 0x00000400),
-
("IN_DONT_FOLLOW", 0x02000000),
-
("IN_EXCL_UNLINK", 0x04000000),
-
("IN_IGNORED", 0x00008000),
-
("IN_ISDIR", 0x40000000),
-
("IN_MASK_ADD", 0x20000000),
-
("IN_MODIFY", 0x00000002),
-
("IN_MOVED_FROM", 0x00000040),
-
("IN_MOVED_TO", 0x00000080),
-
("IN_MOVE_SELF", 0x00000800),
-
("IN_ONESHOT", 0x80000000),
-
("IN_ONLYDIR", 0x01000000),
-
("IN_OPEN", 0x00000020),
-
("IN_Q_OVERFLOW", 0x00004000),
-
("IN_UNMOUNT", 0x00002000),
-
]
-
IN_ALL_EVENTS = 0x00000fff
-
-
# Maximum length of a filename. not including the terminating null
-
NAME_MAX = os.pathconf('.', 'PC_NAME_MAX')
-
# unpack inotify_event
-
EV_FMT = struct.Struct('@i3I')
-
# limit of inotify_event
-
EV_LEN_MAX = EV_FMT.size + NAME_MAX + 1
-
EV_LEN_MIN = EV_FMT.size
-
-
def print_event(filename, mask):
-
now = datetime.datetime.now()
-
print('{:02}:{:02}:{:02},{:03} file({file}) {mask}'.format(
-
now.hour, now.minute, now.second, now.microsecond//1000,
-
file=str(filename, 'utf-8'),
-
mask=' '.join(k for k,v in MASK_EVENTS if mask & v)))
-
-
def main():
-
if len(sys.argv) != 2:
-
sys.exit('usage: {} path'.format(sys.argv[0]))
-
pathname = bytes(sys.argv[1], 'utf-8')
-
libc = CDLL('libc.so.6')
-
-
fd = libc.inotify_init(None)
-
if fd == -1:
-
libc.perror(b'inotify_init')
-
sys.exit()
-
wd = libc.inotify_add_watch(fd, pathname, IN_ALL_EVENTS)
-
if wd == -1:
-
libc.perror(b'inotify_add_watch')
-
sys.exit()
-
-
ev_buffer = create_string_buffer(EV_LEN_MAX)
-
while True:
-
ret = libc.read(fd, ev_buffer, EV_LEN_MAX)
-
if ret < EV_LEN_MIN:
-
libc.perror(b'read')
-
break
-
-
offset = 0
-
while offset < ret:
-
wd, mask, cookie, elen = EV_FMT.unpack_from(ev_buffer, offset)
-
offset += EV_FMT.size
-
filename = ev_buffer[offset: offset + elen].rstrip(b'\x00')
-
print_event(filename if filename else pathname, mask)
-
offset += elen
-
-
libc.inotify_rm_watch(fd, wd)
-
libc.close(fd)
-
-
if __name__ == '__main__':
-
main()
2. 代码分析。因为本文是讨论“Python调用外部代码”,而这是第一个Python和外部代码有多次交互的例子,需要分析几个关键的地方。不管Python和C语法上的差异,大家可能注意到一些实现细节的改变,这些地方恰好能体现出ctypes功能与局限。
行7~30:定义了所有mask的名称和数值,对比C实现的那个相似结构,这里hardcode了数值,因为Python不认识C的宏定义,这些数值只能人肉从inotify.h中提取,如果实践中遇到更复杂的头文件,而所需要的常量包含在众多的条件编译选项中,建议还是写几行C代码来提取这些数值。
行32~38:依然在处理C中的常量,C的NAME_MAX通过Python的os提取,C的struct inotify_event成员分布可以由Python的struct模拟,初步可以看出ctypes的代价了,如果这类常量定义有任何改变,C实现只要一次重编就搞定,Python实现就是噩梦了!
行51:加载glibc,搜索规则与C的dlopen一样,含有“/”则按照路径检索,否则通过程序代码段(
DT_RPATH,gcc编译时指定)-->环境变量(LD_LIBRARY_PATH)-->系统库(/lib, /usr/lib),其实中间还有其他环节,但实践中通过这三个层次应该定位到自己的DLL了。
行57:Python bytes对应C const char *,所以此处pathname必须提前从str转换为bytes。
行64:read系统调用的ev_buffer会写入内容,Python没有内置类型可以容易转换为这种结构,所以ctypes定义了create_string_buffer。
行73:提取struct inotify_event的name成员时,要去掉末尾由于地址对齐填充的‘\0’字节。
3. 输出分析。功能上与第四节的C版本完全一致,只是此处观察目录中几个不同的事件,用make在目录中编译C版本实现,用Python实现观察目录内文件变化。
-
(py3) bash-4.2 $python notify2.py .
-
22:22:35,739 file(.) IN_ISDIR IN_OPEN
-
22:22:35,739 file(.) IN_CLOSE_NOWRITE IN_ISDIR
-
22:22:44,038 file(.) IN_ISDIR IN_OPEN
-
22:22:44,038 file(.) IN_CLOSE_NOWRITE IN_ISDIR
-
22:22:44,038 file(Makefile) IN_OPEN
-
22:22:44,038 file(Makefile) IN_ACCESS
-
22:22:44,038 file(Makefile) IN_CLOSE_NOWRITE
-
22:22:44,850 file(notify1.c) IN_OPEN
-
22:22:44,850 file(notify1.c) IN_ACCESS
-
22:22:44,850 file(notify1.c) IN_CLOSE_NOWRITE
-
22:22:47,223 file(notify1) IN_CREATE
-
22:22:47,223 file(notify1) IN_OPEN
-
22:22:47,429 file(notify1) IN_CLOSE_WRITE
-
22:22:47,438 file(notify1) IN_OPEN
-
22:22:47,438 file(notify1) IN_MODIFY
-
22:22:47,438 file(notify1) IN_MODIFY
-
......
-
22:22:47,441 file(notify1) IN_ACCESS
-
22:22:47,441 file(notify1) IN_MODIFY
-
22:22:47,441 file(notify1) IN_CLOSE_WRITE
-
22:22:47,441 file(notify1) IN_ATTRIB
-
22:23:07,730 file(.) IN_ISDIR IN_OPEN
-
22:23:07,730 file(.) IN_CLOSE_NOWRITE IN_ISDIR
-
22:23:07,730 file(Makefile) IN_OPEN
-
22:23:07,730 file(Makefile) IN_ACCESS
-
22:23:07,730 file(Makefile) IN_CLOSE_NOWRITE
-
22:23:07,731 file(notify1) IN_DELETE
上面的输出就是在另一个窗口先后执行make notify1和make clean的结果,很容易猜到gcc生成目标文件的最后一步就是修改可执行权限(22:22:47,441 file(notify1) IN_ATTRIB)
备忘:
*** int,str,bytes可以直接传入到C API,int会做适当的类型转换,str对应const wchar_t *,bytes对应const char *,其他一切Python类型,都要转换为ctypes的类型才能传入C API
*** Python的Immutable类型规则(特别是str,bytes),也要被C API遵守,比如bytes对应的const char *而不能是char *,违反了该规则不见得立即有问题,就像C的缓冲期溢出和内存泄漏,但可能出问题的地方必将出现问题,出来混总是要还的。
(更多内容参见中篇)
阅读(3271) | 评论(0) | 转发(0) |