目录
1. 前言
2. Python与C交互基础
2.1 C调用Python
2.1.1 简单使用
2.1.2 C调用Python函数
2.1.3 C调用Python基础API
2.2 Python 调用C
2.2.1 使用ctypes模块
2.2.2 使用C为Python编写拓展模块
1. 前言
(本文以Python3为例,Python3是未来,大家都懂的)
Python作为一个功能强大又语法简洁的语言,其应用已无需多言。要想在Android平台运行起Python,也有方案实现,其实质就是在Android系统上搭建Python环境。对此Google已经提供了SL4A(Scripting Layer for Android )方案,支持多种脚本语言,除此之外,还可以使用一个叫QPython的app,可以直接在Android上编写以及运行Python代码。但其实意义不大,写好的Python代码并不是以一个独立的app进程运行的,只不过是在QPython这个应用中运行而已。这两者都不符合我现在要讨论的东西,如题,笔者想要讨论的是如何在Android平台使用Java与Python代码相互调用,换言之,就是如何在Android工程中嵌入一个Python解释器。
首先谈一点,为什么要在Android平台使用Python?Python拥有众多强大的第三方库和框架,在机器学习、大数据处理等诸多方面都有不俗的应用。另外,就语法而言,Python比Java更加简洁,同时又功能强大,既可面向过程亦可面向对象,而不像Java一样,是一种纯粹的面向对象语言,哪怕打印一句话也需要先创建类。Python作为一种脚本语言,可以边解释边执行,而不需编译,另外Python中存在的元类,可以使我们动态的创建类,如此可以在不需要重新编译安装apk的情况下,动态的由远程服务端为Android项目添加功能。我们还可以将Python已有的一些东西移植到Android平台,例如tornado、django等,总之玩法多多。
在Android平台,官方并不支持直接使用Python开发app,基于虚拟机的Java(或kotlin)才是更好的选择,其他语言是无法自如的使用官方Framework提供的api的,尤其是在程序界面的表现上,典型的反例就是kivy。什么是kivy,可自行了解,但要解决Android平台上Java与Python的交互,kivy确实是一个方向,而且是一个醍醐灌顶的方向。kivy实际上已经解决我们需要实现的目的,模仿Android平台上的kivy实现机制即可。但是,kivy使用了大量的Cython技术,而非CPython API接口,需要学习Cython语法,并且在其他一些方面存在一些限制。kivy给我们提供的思路就是借助Java的jni机制,实现Python与Java的交互。即在一个安卓apk工程中包含一个cython.so解释器,通过jni机制调用解释器去解释执行Python代码,通过Java调C,C调Python实现交互。有一点需要说明,Python作为一门胶水语言,Python与C的交互是非常方便的,因此才能实现这一系列调用。
关于该种方案,已有国外网友实践,原理如下
链接地址
除此之外,本博客将通过另外两种方案实现。其中第一种类似上述方案,但集成CPython解释器,非Cython,因此需要掌握如何实现Python与C的交互。
2. Python与C交互基础
2.1 C调用Python
2.1.1 简单使用
(1)流程
创建一个CppUserPythonTest.cpp源文件,再创建一个PythonAppTest.py文件,实现一个printTime函数。
- 初始化Python解析器。
- 执行Python代码,字符串,对象或模块。
- 关闭Python解析器。
(2)环境
- Visual Studio 2019;
- Anaconda version:4.8.3;
- Python version:anconda中的python 3.6.10。
(3)具体实现
首先,File -> New -> Project,创建控制台应用程序。
点击“下一步”,创建项目名为“CppUserPythonTest”。
点击“创建”。
右键点击“CppUserPythonTest”解决方案,添加一个新建项目“PythonAppTest”
此时的解决方案报班两个项目:
在PythonAppTest.py中添加如下代码:
- import datetime
-
- def printTime():
- time_stamp = datetime.datetime.now()
-
- print(time_stamp.strftime('%Y.%m.%d-%H:%M:%S'))
- #print('invoke printTime:'+str(time.time()))
- return (1,)#元组只有一个元素时,需在末尾加逗号
CppUserPythonTest.cpp
文件中添加调用Python的代码
- #include <Python.h>
-
- #include <iostream>
- #include <string>
-
- using namespace std;
-
- void InitPython()
- {
- Py_Initialize(); /*初始化python解释器,告诉编译器要用的python编译器*/
- if (!Py_IsInitialized())
- {
- printf("初始化失败!");
- return;
- }
- PyRun_SimpleString("import sys"); //以下操作是路径设置,
- PyRun_SimpleString("sys.argv = ['python.py']"); //添加
- PyRun_SimpleString("sys.path.append('..')");
- PyRun_SimpleString("sys.path.append('.')");
- PyRun_SimpleString("sys.path.append('../PythonAppTest')"); //python文件的目录路径
- PyRun_SimpleString("sys.path.append('./')");
- }
-
-
- int main(void)
- {
- InitPython();
-
- PyRun_SimpleString("print('hello C !')");
- PyRun_SimpleString("import PythonAppTest");
- PyRun_SimpleString("PythonAppTest.printTime()");
- Py_Finalize();//关闭Python解析器
- return 0;
- }
注意:除了用PyRun_SimpleString函数直接运行代码,还可以使用PyRun_SimpleFile函数运行一个Python脚本。
原型:PyRun_SimpleFile(FILE *fp, const char *filename) ,由于版本差异,使用该方式可能会造成崩溃,推荐另一种替代方式。
PyRun_SimpleString(“execfile(“test.py”)”)
此时会在CppUserPythonTest.cpp
文件中出现很多错误。
(4)项目配置
首先配置Python解释器,即include目录和库目录。
在项目“CppUserPythonTest”上右键,选择“属性”,打开“CppUserPythonTest”的属性页。
设置项目为Release和x64。
添加include目录:
添加libs:
添加链接库:
发现此时可以成功引入Python.h。
(5)执行程序
首先,Build -> Build Solution。
然后,Debug -> Start Debugging。
2.1.2 C调用Python函数
依然使用上面的python文件,将上面的CppUserPythonTest.cpp进行修改。
-
- #include <Python.h>
- #include <iostream>
- #include <string>
-
- using namespace std;
-
- void InitPython()
- {
- Py_Initialize(); /*初始化python解释器,告诉编译器要用的python编译器*/
- if (!Py_IsInitialized())
- {
- printf("初始化失败!");
- return;
- }
- PyRun_SimpleString("import sys"); //以下操作是路径设置,
- PyRun_SimpleString("sys.argv = ['python.py']"); //添加
- PyRun_SimpleString("sys.path.append('..')");
- PyRun_SimpleString("sys.path.append('.')");
- PyRun_SimpleString("sys.path.append('../PythonAppTest')"); //python文件的目录路径
- PyRun_SimpleString("sys.path.append('./')");
- }
-
- int main(void)
- {
- PyObject* module_name, * module, * func, * dic;
- const char* fun_name = "printTime";//需调用的Python函数名
- PyObject* resultValue;
-
- InitPython();
- //导入Python 模块并检验
- module_name = Py_BuildValue("s", "PythonAppTest");
- module = PyImport_Import(module_name);
-
- if (!module)
- {
- printf("import test failed!");
- return -1;
- }
-
- //获取模块中的函数列表,是一个函数名和函数地址对应的字典结构
- dic = PyModule_GetDict(module);
- if (!dic)
- {
- printf("failed !\n");
- return -1;
- }
-
- func = PyDict_GetItemString(dic, fun_name);
- if (!PyCallable_Check(func))
- {
- printf("not find %s\n", fun_name);
- return -1;
- }
-
- int r;
- //获取Python函数返回值,是一个元组对象
- resultValue = PyObject_CallObject(func, NULL);
- PyArg_ParseTuple(resultValue, "i", &r);
- printf("result :%d\n", r);
-
- Py_DECREF(module);
- Py_DECREF(dic);
- Py_Finalize();
- return 0;
- }
输出为:
2.1.3 C调用Python基础API
C 调用Python API | Python 对应 |
---|---|
PyImport_ImportModel |
import module |
PyImport_ReloadModule |
reload(module) |
PyImport_GetModuleDict |
module._dict_ |
PyDict_GetItemString |
dict[key] |
PyDict_SetItemString |
dict[key] = value |
PyDict_New |
dict = {} |
PyObject_GetAttrString |
getattr(obj, attr) |
PyObject_SetAttrString |
setattr(obj, attr, val) |
PyObject_CallObject |
funcobj(*argstuple) |
PyEval_CallObject |
funcobj(*argstuple) |
PyRun_String |
eval(exprstr) , exec(stmtstr) |
PyRun_File |
exec(open(filename().read()) |
(1)Py_BuildValue()函数
作用:将C/C++类型的数据转变成PyObject*对象。
原型:PyAPI_FUNC(PyObject*) Py_BuildValue(const char *format, ...);
参数解释:
format及转换格式,类似与C语言中%d,%f,后面的不定参数对应前面的格式,具体格式如下:
“s”(string) [char *] :将C字符串转换成Python对象,如果C字符串为空,返回NONE。
“s#”(string) [char *, int] :将C字符串和它的长度转换成Python对象,如果C字符串为空指针,长度忽略,返回NONE。
“z”(string or None) [char *] :作用同”s”。
“z#” (stringor None) [char *, int] :作用同”s#”。
“i”(integer) [int] :将一个C类型的int转换成Python int对象。
“b”(integer) [char] :作用同”i”。
“h”(integer) [short int] :作用同”i”。
“l”(integer) [long int] :将C类型的long转换成Pyhon中的int对象。
“c”(string of length 1) [char] :将C类型的char转换成长度为1的Python字符串对象。
“d”(float) [double] :将C类型的double转换成python中的浮点型对象。
“f”(float) [float] :作用同”d”。
“O&”(object) [converter, anything] :将任何数据类型通过转换函数转换成Python对象,这些数据作为转换函数的参数被调用并且返回一个新的Python对象,如果发生错误返回NULL。
“(items)”(tuple) [matching-items] :将一系列的C值转换成Python元组。
“[items]”(list) [matching-items] :将一系列的C值转换成Python列表。
“{items}”(dictionary) [matching-items] :将一系类的C值转换成Python的字典,每一对连续的C值将转换成一个键值对。
例子:
后面为PyObject的返回值
- Py_BuildValue("") None
- Py_BuildValue("i",123) 123
- Py_BuildValue("iii",123, 456, 789) (123, 456, 789)
- Py_BuildValue("s","hello") 'hello'
- Py_BuildValue("ss","hello", "world") ('hello', 'world')
- Py_BuildValue("s#","hello", 4) 'hell'
- Py_BuildValue("()") ()
- Py_BuildValue("(i)",123) (123,)
- Py_BuildValue("(ii)",123, 456) (123, 456)
- Py_BuildValue("(i,i)",123, 456) (123, 456)
- Py_BuildValue("[i,i]",123, 456) [123, 456]
- Py_BuildValue("{s:i,s:i}", "abc",123, "def", 456) {'abc': 123, 'def': 456}
- Py_BuildValue("((ii)(ii))(ii)", 1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
(2)PyArg_ParseTuple函数
作用:此函数其实相当于sscanf(str,format,…),是Py_BuildValue的逆过程,这个函数将PyObject参数转换成C/C++数据类型,传递的是指针,但这个函数与Py_BuildValue有点不同,这个函数只能解析Tuple元组,而Py_BuildValue函数可以生成元组,列表,字典等。
原型:PyAPI_FUNC(int) PyArg_ParseTuple(PyObject *args, const char *format,...)
- args:一般为Python程序返回的元组。
- format:与Py_BulidValue类型,就不在累述咯。
-
- 元组操作函数:
- 因为程序之间传递的参数,大多数为Tuple类型,所以有专门的函数来操作元组:
-
- PyAPI_FUNC(PyObject *) PyTuple_New(Py_ssize_t size);
- 解释:新建一个参数列表(调试了下,发现其实是用链表实现的),size列表为长度的宽度
-
- PyAPI_FUNC(Py_ssize_t) PyTuple_Size(PyObject *);
- 解释:获取该列表的大小
-
- PyAPI_FUNC(PyObject *) PyTuple_GetItem(PyObject *, Py_ssize_t);
- 解释:获取该列表某位置的值
-
- PyAPI_FUNC(int) PyTuple_SetItem(PyObject *,Py_ssize_t, PyObject *);
- 解释:设置该列表此位置的值。如PyTuple_SetItem(pyParams,1,Py_BuildValue("i",2));设置第2个位置的值为2的整数。
备注:对应的列表和字典也有对应的操作。
更多的接口调用以及数据类型转化,参照Python文档。
2.2 Python 调用C
Python调用C的方法有多种,见https://blog.csdn.net/jwh_bupt/article/details/8287416。这里只介绍种方式:
2.2.1 使用ctypes模块
Python文档有详细示例
2.2.2 使用C为Python编写拓展模块
Python之所以如此强大,正是由于可以使用C\C++为其编写拓展模块,手动编写拓展模块的方式稍微有些繁琐,可借用SWIG自动实现,简洁快速。更多详细的SWIG用法,见其官方文档。
官网下载 windows包( swigwin-4.0.2)并解压。将包含swig.exe的文件路径加到环境变量path中。
(1)直接使用SWIG和命令行
(1.1)创建源文件和swig接口文件。
创建头文件mytest.h
- //mytest.h
- int add(int a,int b);
- int sub(int a,int b);
创建源文件mytest.cpp
- //mytest.cpp
- int add(int a, int b){ return a+ b;}
- int sub(int a,int b){ return a - b;}
创建swig接口文件mytest.i,并在其中添加如下代码
- %module mytest
- %{
- #define SWIG_WITH_INIT
- #include "mytest.h"
- %}
- %include "mytest.h"
此处.i文件为SWIG的接口文件,其中%module
后面定义模块名,用%inline
定义方法列表
- %inline %{
- 包含导出的函数
- %}
%module 后面的名字是被封装的模块名称。封装口,python通过这个名称加载程序。
%{ %}之间所添加的内容,一般包含此文件需要的一些函数声明和头文件。
最后一部分,声明了要封装的函数和变量。
(1.2) 在命令行中运行
swig -c++ -python mytest.i
如果是C语言的话就是
swig -python mytest.i
执行完,在当前目录下会生成两个文件mytest_wrap.cxx和mytest.py。
(1.3)编写setup.py文件
- from distutils.core import setup
- from distutils.extension import Extension
-
- test_module = Extension('_mytest', sources=['mytest_wrap.cxx', 'mytest.cpp'],)
-
- setup(name = 'mytest',
- version = '0.1',
- author = 'SWIG Docs',
- description = 'Simple swig pht from docs',
- ext_modules = [test_module],
- py_modules = ['mytest'])
执行该setup.py文件
python setup.py build
执行完之后会在同级目录的build文件夹的lib文件夹下生成对应的.pyd文件和mytest.py文件;
之后要注意:要在lib这个目录下编写调用这个C++模块的py脚本!因为执行完setup.py之后在setup.py的同级目录下也会生成一个mytest.py文件,但没有对应的.pyd文件,直接在这个里面编写py脚本进行调用的话会由于没有动态链接库而报错!
(1.4)编写python脚本调用C++
- import mytest
-
- a = mytest.add(1, 2)
- print(a)
-
- b = mytest.sub(2, 1)
- print(b)
注意,生成的.pyd文件也可以是.so或者.dll的形式。
2.3 C与Python交互总结
有了Python与C的交互基础,则还需要Android中的NDK开发基础,关于Android平台的jni调用,本文不在此处详解,可看看JNI方面博客,而此处我们需要使用Crystax NDK开发工具链,非官方NDK工具链,需自行下载。下一篇正式涉及Python for Android。