一、写在前面
1、我们可以用 C 语言为 Python 写一些模块以便提升运行效率,下面我们就用‘99宿舍’四六级成绩查询加密算法的例子来简单介绍一下 python2 的 C 扩展基本结构。
2、‘99宿舍’客户端使用 DES_CFB64 算法来加密 HTTP 通讯数据。另外,加密发送数据、解密接收到的数据使用的是不同的 key ,具体可以通过逆向获取。
二、C 模块的基本框架
1、导出函数的结构
先介绍一段代码中执行逻辑处理的代码,比如我们的功能就是返回“Hello,World”字符串:
1 2 3 4 5 6 7 8 9 |
char *do_foo() { return "Hello, World"; } static PyObject* foo(PyObject* self, PyObject* args) { return Py_BuildValue("s", do_foo()); } |
这里有两个函数,普通的 C 函数 do_foo() 和 可被 Python 接口识别调用的 foo()
注意传递的参数列表:(PyObject* self, PyObject* args), 还有其他两种格式,具体我们在第三节解释
2、函数列表/注册函数
我们通过一个数组记录上面写好的导出函数/接口,这个就是 static PyMethodDef ExtendMethods[],示例代码:
1 2 3 4 5 |
static PyMethodDef FooMethods[] = { {"foo", foo, METH_VARARGS, "foo() from C module "}, {NULL, NULL, 0, NULL}, }; |
PyMethodDef 结构体的定义:
1 2 3 4 5 6 |
struct PyMethodDef { char* ml_name; // 暴露给 Python 的函数名 PyCFunction ml_meth; // 函数地址 int ml_flags; // 一些选项、开关 char* ml_doc; // 一些简单说明 }; |
针对 'int ml_flags' 这个field,后面再详细介绍
3、模块初始化
调用 Py_InitModule() 来初始化/注册模块,另外调用Py_InitModule()的函数需要被 PyMODINIT_FUNC 所修饰
PyMODINIT_FUNC 的定义在 /usr/include/python2.7/pyport.h 中,在 C 里面为void,C++ 里面则是extern "C"
示例代码:
1 2 3 |
PyMODINIT_FUNC FooInit(){ Py_InitModule("foo", FooMethods); } |
三、C 接收 Python 对象作为参数
1、导出函数申明
对于如何接受参数以及接受什么样的参数,主要有以下三种格式:
1 2 3 4 5 6 7 8 |
// 不传递输入参数,参数self只在C函数被实现为内联方法(built-in method)时才被用到,通常该参数的值为空(NULL) PyObject* FooWithoutArgs(PyObject* self); // 传递普通参数,参数将会以 tuple 类型传递 PyObject* Foo(PyObject* self, PyObject* args); // 传递普通参数和关键词参数,关键词参数将以 Dictionary 类型传递 PyObject* FooWithKeywords(PyObject* self, PyObject* args, PyObject* kw); |
本文没有涉及第三种传递关键词参数的介绍,具体可以参考:https://docs.python.org/2/extending/extending.html#keyword-parameters-for-extension-functions
2、注册函数时的 flag
上面的三种参数格式,在 函数列表 ( PyMethodDef [] ) 中注册函数对应的三种 flag 选择如下:
1 2 3 |
METH_NOARGS // 没有参数 METH_VARARGS // tuple 参数 METH_VARARGS|METH_KEYWORDS // tuple 参数 + dict 关键词 |
3、Python 对象转换为 C 的基本类型
我们需要将 PyObject* args 里面包含的参数转换为 C 语言里面对应的基本类型,我们使用的函数是:int PyArg_ParseTuple
(PyObject *args, const char *format, ...)
其他方法和支持的变量类型请见:https://docs.python.org/2/c-api/arg.html
示例代码:
1 2 3 4 5 6 7 8 9 10 |
static PyObject* Foo(PyObject* self, PyObject* args) { char* data_str; int data_int; float data_float; PyArg_ParseTuple(args, "sif", &data_str, &data_int, &data_float); // 注意:这里尽管字符串应该已经是指针了,但是我们的目的是传递字符串,一个已经在内存中存在的字符串 // 所以是取用户定义的字符串变量的地址,然后重写那个内存区的值,这样我们的变量就指向了需要的字符串了 } |
四、C 返回值转为 Python 对象
1、返回非空值
使用 PyObject* Py_BuildValue
(const char *format, ...) 构造返回值
如果 char *format 字符串为空,则返回None;
如果 char *format 字符串仅包含一个元素,则返回的对象类型就是那个元素的类型;
如果 char *format 字符串为多个元素,则返回一个 tuple
具体支持的变量类型:https://docs.python.org/2/c-api/arg.html#c.Py_BuildValue
示例代码:
1 2 3 4 5 6 7 |
static PyObject* Foo(PyObject* self, PyObject* args) { char* data_str = “Hello, World”; int data_int = 2017; float data_float = 3.14159; return Py_BuildValue("sif", data_str, data_int, data_float); } |
2、返回空值
1 2 3 4 |
static PyObject* Foo(PyObject* self, PyObject* args) { return Py_None; } |
五、完整的 DES_CFB64 模块实现
1、des_cfb64.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * filename: des_cfb64.h * author: King's Way <root#kings-way.info> * date: 20170211 */ #include <string.h> #include <stdlib.h> #include <openssl/des.h> #ifndef _DES_CFB64_H #define _DES_CFB64_H char *encrypt( char *key, char *data, int size); char *decrypt( char *key, char *data, int size); #endif |
2、des_cfb64.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* * filename: des_cfb64.c * author: King's Way <root#kings-way.info> * date: 20170211 */ #include"des_cfb64.h" char *encrypt( char *key, char *data, int size) { int n = 0; static char* result; result = (char *)malloc(size); des_cblock block; des_key_schedule schedule; memcpy(block, key, 8); DES_set_odd_parity(&block); DES_set_key_checked(&block, &schedule); DES_cfb64_encrypt(data, result, size, &schedule, &block, &n, DES_ENCRYPT); return result; } char *decrypt( char *key, char *data, int size) { int n = 0; static char* result; result = (char *)malloc(size); des_cblock block; des_key_schedule schedule; memcpy(block, key, 8); DES_set_odd_parity(&block); DES_set_key_checked(&block, &schedule); DES_cfb64_encrypt(data, result, size, &schedule, &block, &n, DES_DECRYPT); return result; } |
3、desdemo.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/* * filename: desdemo.c * author: King's Way <root#kings-way.info> * date: 20170211 */ #include <stdio.h> #include <string.h> #include "des_cfb64.h" #include <Python.h> char *hello() { return "Hello, World"; } static PyObject* __hello(PyObject* self, PyObject* args) { return Py_BuildValue("s", hello()); } static PyObject* __des_cfb64_encrypt(PyObject* self, PyObject* args) { char *key; char *data; int size; PyArg_ParseTuple(args, "ss", &key, &data); size=strlen(data); // printf("Key:%s, Data:%s\n", key,data); char *result = encrypt(key, data, size); return Py_BuildValue("s", result); } static PyObject* __des_cfb64_decrypt(PyObject* self, PyObject* args) { char *key; char *data; int size; PyArg_ParseTuple(args, "ss", &key, &data); size=strlen(data); char *result = decrypt(key, data, size); return Py_BuildValue("s", result); } static PyMethodDef ExtendMethods[] = { {"hello", __hello, METH_VARARGS, "HelloWorld() from C"}, {"des_cfb64_encrypt", __des_cfb64_encrypt, METH_VARARGS, "DES_CFB64_Encrypt() from OpenSSL"}, {"des_cfb64_decrypt", __des_cfb64_decrypt, METH_VARARGS, "DES_CFB64_Decrypt() from OpenSSL"}, {NULL, NULL, 0, NULL}, }; PyMODINIT_FUNC initdesdemo(){ Py_InitModule("desdemo", ExtendMethods); } |
六、编译&链接
1、手写 Makefile 编译生成动态链接库
运行命令 make 编译生成 so 文件,然后在当前目录下启动python,import desdemo 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
cc = gcc cflags = -static objects_py = des_cfb64.o desdemo.o desdemo.so: desdemo.o des_cfb64.o cc -shared -fPIC $(objects_py) -I/usr/include/openssl -lcrypto -o desdemo.so des_cfb64.o: des_cfb64.c des_cfb64.h cc $(cflags) -fPIC -c des_cfb64.c desdemo.o: desdemo.c cc $(cflags) -fPIC -c desdemo.c -I/usr/include/python2.7 .PHONY : clean clean : -rm $(objects_py) desdemo.so |
2、使用 python 推荐的 setup.py
准备好 setup.py,python setup.py build 进行编译,然后进入build/lib* 目录找到 .so 文件,即可import使用
或者使用 python setup.py install 将 .so 文件安装到python系统路径里(需要root权限)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from distutils.core import setup, Extension setup( name = 'desdemo', version = '0.1', description='DES_CFB64 Encrypt/Decrypt Function From OpenSSL', author='King\'s Way', author_email='root#kings-way.info', url='https://github.com/kings-way/cet6_crypt', ext_modules = [ Extension( 'desdemo', ['desdemo.c', 'des_cfb64.c'], # include_dirs=['/usr/include/openssl'], libraries=['crypto'] ) ] ) |
七、参考资料
1. Python 文档:模块编写 https://docs.python.org/2/extending/extending.html
2. Python 文档:参数处理 https://docs.python.org/2/c-api/arg.html#c.PyArg_ParseTuple
3. IBM DeveloperWorks: 用C语言扩展Python的功能
4. 使用C写Python的模块:https://www.zouyesheng.com/python-module-c.html
5. Python 文档:setup脚本 https://docs.python.org/2/distutils/setupscript.html
6. cnblog: 使用C语言扩展Python(一)