LongLong's Blog

分享IT技术,分享生活感悟,热爱摄影,热爱航天。

使用Python调用C函数

脚本语言经常会作为一种“胶水语言”来使用,来完成对各种外来组建的整合控制功能,如此能够将外部的功能简单的进行导入成为了一种脚本语言是否好用的关键。之前一直使用Lua做类似的工作,其优点就是轻量级,可以直接使用直接编译的标准so,缺点主要是功能过于有限,不适合完成比较复杂的工作。而由于Raspberry PI主要使用Python作为核心编程语言,这里就尝试使用Python来完成整合任务,其中关键的步骤就是使用Python调用C的函数。以下的方法仅适用于Python2。

1. 直接调用C的库函数

直接使用ctypes可以直接引入C函数库,然后直接调用即可,代码如下

from ctypes import *;
libc = cdll.LoadLibrary('/usr/lib/libc.dylib');
libc.printf("Hello World!\n");

2. 编写可用Python调用的C函数

与编写Lua可调用的C函数类似,Python可调用的C函数也为特定的模式,并通过编译为so的方式提供给Python使用,以下为简单的一个例子

#include <Python.h>

PyObject* wrap_demo_func(PyObject* self, PyObject* args) {
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
        return NULL;
    }
    return Py_BuildValue("i", a + b);
}

static PyMethodDef demoMethods[] = {
    {"demo_func", wrap_demo_func, METH_VARARGS, "demo func"},
    {NULL, NULL}
};

extern "C" void initdemo() {
    Py_InitModule("demo", demoMethods);
}

编译的命令如下

g++ -g -Wall -I /usr/include/python2.7/ -fpic --shared demo.cc -o demo.so

运行的Python脚本如下

import demo;
print demo.demo_func(1, 2);

运行后会输出3

3. 编写Python调用的C函数中的一些处理

调用一个C++类中的方法

#include <Python.h>
class Demo {
public:
    void func() {
        printf("Hello World!\n");
    }
};
static void del_demo(PyObject *obj) {
    delete (Demo*) PyCapsule_GetPointer(obj, "Demo");
}
PyObject* wrap_create_demo(PyObject* self, PyObject* args) {
    Demo* demo = new Demo();
    return PyCapsule_New(demo, "Demo", del_demo);
}

PyObject* wrap_demo_func(PyObject* self, PyObject* args) {
    Demo* demo;
    PyObject* p_demo;
    if (!PyArg_ParseTuple(args,"O", &p_demo)) {
        return NULL;
    }
    if (!(demo = (Demo*) PyCapsule_GetPointer(p_demo, "Demo"))) {
        return NULL;
    }
    demo->func();
    return Py_BuildValue("");
}

static PyMethodDef demoMethods[] = {
    {"create_demo", wrap_create_demo, METH_VARARGS, "create demo"},
    {"demo_func", wrap_demo_func, METH_VARARGS, "demo func"},
    {NULL, NULL}
};

extern "C" void initdemo() {
    Py_InitModule("demo", demoMethods);
}

运行的Python脚本如下

import demo;
d = demo.create_demo();
demo.demo_func(d);

运行后输出Hello World!

使用Epoll实现Echo Server

由于一些需求,需要在PC上读取超声波传感器的数据,而超声波传感器使用的是I2C/TTL接口,虽然可以使用TTL转USB来接入PC,但是TTL接口只能够支持同时连接两个超声波传感器,而这里可能需要使用6个。

想到的一只解决办法是将超声波传感器连接到Raspberry PI的GPIO的I2C接口上,然后PC和Raspberry PI使用WIFI进行通信。这里需要在Raspberry PI上启动一个TCP Server,PC通过调用此TCP Server的接口获取超声波传感器的测量数据。虽然也可以让Raspberry PI将超声波传感器的数据发送到PC,但这样就需要让超声波传感器一直处于工作状态,会带来较大的能耗。

1. 实现一个简单的Echo Server

Echo Server可以作为一个最简单的TCP Server的模版,通过简单的修改就可以实现其他的TCP通信服务。一般的TCP Echo Server实现大致如下,由于Raspberry PI中的主流编程语言是Python,以下代码均使用Python。

from SocketServer import BaseRequestHandler, TCPServer;

class ServerHandler(BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024);
        self.request.send(data);

server = TCPServer(('', 1987), ServerHandler);
server.serve_forever();

测试效果如下

Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello
Hello
Connection closed by foreign host.

这里使用了Python中的TCPServer框架,但一般的TCPServer框架所实现的方式基本是类似HTTP Server,即处理完成后就关闭连接,并不能支持连接的服务。而获取超声波传感器的频率可能是比较高的,因此如果每次都重新建立连接会产生较大的开销,同时也增加了测量的延迟。

2. Linux的Epoll系统调用

Epoll是Linux 2.6中支持的路复用IO接口,是对select/poll的增强版本,显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用效率。目前大多数Linux下的应用程序(Nginx Redis等)都使用了Epoll方式进行网络IO操作。同时Epoll提供了水平触发(LT)和边界触发(ET)两种IO事件。

3. 使用Epoll实现Echo Server

这里我们只能直接使用Epoll调用来实现连接复用的Echo Server,其中关键是将数据输出之后重新调整为接收数据的模式,其实现的代码大致如下

import socket;
import select;

#开启一个Socket
HOST = '';
PORT = 1987
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((HOST, 1987));
sock.listen(1);

#初始化Epoll
epoll = select.epoll();
epoll.register(sock.fileno(), select.EPOLLIN);

#连接和接受数据
conns = {};
recvs = {};

try:
    while True:
        #等待事件发生
        events = epoll.poll(1);
        #事件循环    
        for fd, event in events:
            #如果监听的Socket有时间则接受新连接
            if fd == sock.fileno():
                client, addr = sock.accept();
                client.setblocking(0);
                #注册新连接的输入时间
                epoll.register(client.fileno(), select.EPOLLIN);
                conns[client.fileno()] = client;
                recvs[client.fileno()] = '';
            elif event & select.EPOLLIN:
                #读取数据
                while True:
                    try:
                        buff = conns[fd].recv(1024);
                        if len(buff) == 0:
                            break;
                    except:
                        break;            
                    recvs[fd] += buff;
                #调整输出事件
                if len(buff) != 0:
                    epoll.modify(fd, select.EPOLLOUT);
                else:
                #如果数据为空则连接已断开
                    epoll.modify(fd, select.EPOLLHUP);
            elif event & select.EPOLLOUT:
                #发送数据
                try:
                    n = conns[fd].send(recvs[fd]);
                    recvs[fd] = '';
                    #重新调整为接收数据
                    epoll.modify(fd, select.EPOLLIN);
                except:
                    epoll.modify(fd, select.EPOLLHUP);
            elif event & select.EPOLLHUP:
                #关闭清理连接
                epoll.unregister(fd);
                conns[fd].close();
                del conns[fd];
                del recvs[fd];
finally:
    epoll.unregister(sock.fileno());
    epoll.close();
    sock.close();

4. 测试效果

使用Telnet连接上Echo Server,先输入Hello,会返回Hello,再输入Hi,则会继续返回Hi

Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello
Hello
Hi
Hi