先说句题外话:无论是在C中还是Java中调用Python,当遇到多线程的时候一定要想到GIL锁的存在。
在Python中调用C/C++代码:这也是最常见的混合编程方式。并且有很多优秀的开源项目可以帮助我们实现这种场景,比如pybind11.
在C/C++中调用Python代码:Python也为这种场景提供了丰富的接口。
Java中调用C/C++:也可以通过JNI实现Java与C/C++的相互调用。
那么Python和Java之间是否能够实现相互调用呢?
不难得到肯定的答案,至少可以通过Python->C/C++->Java的方式实现。
如果你了解Cython这个神器,就可以知道从Python到C是十分简单的。然后再到Java也就不是问题了。虽然想到了这个思路,但是为了更快的达到目的,试验之前还是在网上找了下,果然找到一篇十分不错的文章:
Python一键转Jar包,Java调用Python新姿势! - 掘金 (juejin.cn)
但在过程中还是遇到了一些细节问题,在这篇文章的基础上整理了下面的内容。
这一块儿和文档中介绍的稍有不同。新建一个java工程,将这个文件加到里面去。
先把Java代码放上看起来有点本末倒置,但是后面会发现,在写接下来的C代码时会用到这个工程,来生成对应的C接口名。
package solution.src;
public class Test {
static {
System.load("/home/yourpath/python_C_Java/python/Test.cpython-36m-x86_64-linux-gnu.so");
}
public native void initModule();
public native void uninitModule();
public native String testFunction(String param);
public static void main(String[] args) {
Test tester = new Test();
tester.initModule();
String result = tester.testFunction("this is called from java");
tester.uninitModule();
System.out.println(result);
}
}
我们的目的就是让这些接口在Java中发挥它的作用,但这还不是在Java中直接调用的接口。这里照搬文章中的代码,但里面Python_API_TestFunction的函数名,之前是JNI_API_testFunction。在我的环境里面这个函数名字无论如何都找不到,后来自己写了个更简单的测试函数,测试成功。再后来又把我自己定义的函数删掉,改回JNI_API_testFunction又可以了,现在想应该是在网上拷贝代码时格式的问题。但这里还是用修改后的名字吧,因为JNI_这种格式在一些情况下是有固定含义的。总之运行时若提示找不到函数,则先怀疑一下Python代码本身是否有问题。
# FileName: Test.py
# 示例代码:将输入的字符串转变为大写
def logic(param):
print('this is a logic function')
print('param is [%s]' % param)
return param.upper()
# 接口函数,导出给Java Native的接口
def Python_API_TestFunction(param):
print("enter JNI_API_test_function")
result = logic(param)
print(result)
return result
Java中直接调用的是C接口,而C接口封装了上面的Python接口。
//main.c
#include
#include
#include
#ifndef _Included_main
#define _Included_main
#ifdef __cplusplus
extern "C"
{
#endif
#if PY_MAJOR_VERSION < 3
#define MODINIT(name) init##name
#else
#define MODINIT(name) PyInit_##name
#endif
PyMODINIT_FUNC MODINIT(Test)(void);
JNIEXPORT void JNICALL Java_solution_src_Test_initModule(JNIEnv *env, jobject obj)
{
PyImport_AppendInittab("JavaTest", MODINIT(Test));
Py_Initialize();
PyRun_SimpleString("import os");
PyRun_SimpleString("__name__ = \"__main__\"");
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");
PyObject *m = PyInit_Test();
if (!PyModule_Check(m))
{
PyModuleDef *mdef = (PyModuleDef *)m;
PyObject *modname = PyUnicode_FromString("__main__");
m = NULL;
if (modname)
{
m = PyModule_NewObject(modname);
Py_DECREF(modname);
if (m)
PyModule_ExecDef(m, mdef);
}
}
PyEval_InitThreads();
}
JNIEXPORT void JNICALL Java_solution_src_Test_uninitModule(JNIEnv *env, jobject obj)
{
Py_Finalize();
}
JNIEXPORT jstring JNICALL Java_solution_src_Test_testFunction(JNIEnv *env, jobject obj, jstring string)
{
const char *param = (char *)(*env)->GetStringUTFChars(env, string, NULL);
static PyObject *s_pmodule = NULL;
static PyObject *s_pfunc = NULL;
if (!s_pmodule || !s_pfunc)
{
s_pmodule = PyImport_ImportModule("JavaTest");
s_pfunc = PyObject_GetAttrString(s_pmodule, "Python_API_TestFunction");
// s_pfunc = PyObject_GetAttrString(s_pmodule, "JNI_API_testFunction");
}
PyObject *pyRet = PyObject_CallFunction(s_pfunc, "s", param);
(*env)->ReleaseStringUTFChars(env, string, param);
if (pyRet)
{
jstring retJstring = (*env)->NewStringUTF(env, PyUnicode_AsUTF8(pyRet));
Py_DECREF(pyRet);
return retJstring;
}
else
{
PyErr_Print();
return (*env)->NewStringUTF(env, "error");
}
}
#ifdef __cplusplus
}
#endif
#endif
里面的代码基本照搬了文章中的代码,但是里面的接口名称是不一样的。那么这些接口名称是如何的来的呢?如果了解通过JNI实现Java和C/C++的交互方式,你会对这一部分比较了解。关于Java和C/C++的交互,网上很多文章,我也写过一个:
JNI实现Java调用C/C++代码及对C/C++动态库的单步调试_zx_glave的博客-CSDN博客_java调用c++动态库
在你的Java目录,Test.java所在路径下敲下面的命令:
javac -h . Test.java
在相同目录下得到一个.h文件,打开这个.h文件,里面有我们需要的C接口名称,替换它就好了。
setup.py文件的格式可以单独学习一下,它的作用大致就相当于C语言中的cmake。它不仅可以用于cython,在很多其他方面也是用途广泛。
from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension
sourcefiles = ['JavaTest.pyx', 'main.c']
extensions = [Extension("Test", sourcefiles,
include_dirs=['/usr/java/jdk1.8.0_144/include/',
'/usr/java/jdk1.8.0_144/include/linux/',
'/usr/local/python3/Python-3.6.4/Modules/_ctypes/darwin/'],
library_dirs=['/usr/lib64'],
# extra_link_args=['-fPIC'],
libraries=['python3.6m'])]
setup(ext_modules=cythonize(extensions, language_level = 3))
文件中涉及到一些路径和库名。这跟具体环境有关。
如果看一下c文件,会发现里面用了jni.h,并且需要python环境。
可以在你的计算机中搜索jni.h,再搜一下jni_md.h,这两个是关于jni的;
再搜一下python.h,这是python相关的。这几个路径加到include_dirs中。
再搜一下你使用的python3.6m 库,这里根据你使用的python版本来,把它的路径写到libraries里面。
注意用动态库,如果它找到的是静态库,可能编译会有问题,提示recompile with -fPIC的一大堆错误,但是真的加上-fPIC也还是不行(被注释掉的那一行就是这个作用),后面可能需要研究下为啥。
把它与JavaTest.pyx与main.c放入同一个工程中,在所在的目录下敲下面的命令:
python3.6 setup.py build_ext --inplace
如果不出问题就会生成几个文件,里面就包括Test.cpython-36m-x86_64-linux-gnu.so。回头看一下java工程,里面load的就是这个文件。
运行一下Java工程试试吧。应该打印出下面的内容:
enter JNI_API_test_function
this is a logic function
param is [this is called from java]
THIS IS CALLED FROM JAVA
THIS IS CALLED FROM JAVA
五、补充:多个python模块
在正式应用中,往往存在多个python模块,在上面的基础上,需要注意一些点。
(1)setup.py中需要加入所有的python模块,往往这个模块已经被改为了.pyx.比如要加一个新的JavaTest2.pyx
from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension
sourcefiles = ['JavaTest.pyx', 'JavaTest2.pyx', 'main.c']
extensions = [Extension("Test", sourcefiles,
include_dirs=['/usr/java/jdk1.8.0_144/include/',
'/usr/java/jdk1.8.0_144/include/linux/',
'/usr/local/python3/Python-3.6.4/Modules/_ctypes/darwin/'],
library_dirs=['/usr/lib64'],
# extra_link_args=['-fPIC'],
libraries=['python3.6m'])]
setup(ext_modules=cythonize(extensions, language_level = 3))
(2) 一定要注意的是,c文件也需要修改。添加的每一个模块都要进行初始化。参见下面代码段里面的注释。
//main.c
#include
#include
#include
#ifndef _Included_main
#define _Included_main
#ifdef __cplusplus
extern "C"
{
#endif
#if PY_MAJOR_VERSION < 3
#define MODINIT(name) init##name
#else
#define MODINIT(name) PyInit_##name
#endif
PyMODINIT_FUNC MODINIT(JavaTest)(void);//注意这里很关键,宏入参要改成你的python模块名称,每个模块都要定义一次
PyMODINIT_FUNC MODINIT(JavaTest2)(void);
JNIEXPORT void JNICALL Java_solution_src_Test_initModule(JNIEnv *env, jobject obj)
{
PyImport_AppendInittab("JavaTest", MODINIT(JavaTest));//这里也要改,其实引号里面的内容可以随便,但注意后面用这个模块的地方也要用你自己取的名字
PyImport_AppendInittab("JavaTest2", MODINIT(JavaTest2));
Py_Initialize();
PyRun_SimpleString("import os");
PyRun_SimpleString("__name__ = \"__main__\"");
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");
PyObject *m = PyInit_JavaTest();//这里也要改,PyInit_是头,后面改成你的模块名称,这个函数是在python生成的C代码中定义的,名字应该跟其保持一致。每一个模块都要又这个初始化。
if (!PyModule_Check(m))
{
PyModuleDef *mdef = (PyModuleDef *)m;
PyObject *modname = PyUnicode_FromString("__main__");
m = NULL;
if (modname)
{
m = PyModule_NewObject(modname);
Py_DECREF(modname);
if (m)
PyModule_ExecDef(m, mdef);
}
}
m = PyInit_JavaTest2();//新加一个模块初始化,PyInit_是头,后面改成你的模块名称,这个函数是在python生成的C代码中定义的,名字应该跟其保持一致。每一个模块都要又这个初始化。
if (!PyModule_Check(m))
{
PyModuleDef *mdef = (PyModuleDef *)m;
PyObject *modname = PyUnicode_FromString("__main__");
m = NULL;
if (modname)
{
m = PyModule_NewObject(modname);
Py_DECREF(modname);
if (m)
PyModule_ExecDef(m, mdef);
}
}
PyEval_InitThreads();
}
JNIEXPORT void JNICALL Java_solution_src_Test_uninitModule(JNIEnv *env, jobject obj)
{
Py_Finalize();
}
JNIEXPORT jstring JNICALL Java_solution_src_Test_testFunction(JNIEnv *env, jobject obj, jstring string)
{
const char *param = (char *)(*env)->GetStringUTFChars(env, string, NULL);
static PyObject *s_pmodule = NULL;
static PyObject *s_pfunc = NULL;
if (!s_pmodule || !s_pfunc)
{
s_pmodule = PyImport_ImportModule("JavaTest");//这里的调用,要跟初始化函数中初始化的名称一致
s_pfunc = PyObject_GetAttrString(s_pmodule, "Python_API_TestFunction");
// s_pfunc = PyObject_GetAttrString(s_pmodule, "JNI_API_testFunction");
}
PyObject *pyRet = PyObject_CallFunction(s_pfunc, "s", param);
(*env)->ReleaseStringUTFChars(env, string, param);
if (pyRet)
{
jstring retJstring = (*env)->NewStringUTF(env, PyUnicode_AsUTF8(pyRet));
Py_DECREF(pyRet);
return retJstring;
}
else
{
PyErr_Print();
return (*env)->NewStringUTF(env, "error");
}
}
#ifdef __cplusplus
}
#endif
#endif
这一步骤其实需要对C代码里面的几个函数结构有一些了解,才能懂得里面修改的逻辑。
六、其他事项
(1)setup.py中的路径引用是为了能够找到相应的文件,或者是.h,或者是库文件,但是在每个计算机中,软件的安装路径可能不相同,其实一个方式是把所需的.h都放到我们的工程路径下,把引用的路径改成相对路径,这样就可以免去移植的不便。
from distutils.core import setup
from Cython.Build import cythonize
from distutils.extension import Extension
sourcefiles = ['JavaTest.pyx', 'JavaTest2.pyx', 'main.c']
extensions = [Extension("Test", sourcefiles,
include_dirs=['include'], #这里改成了相对路径,里面包含了dlfcn.h,jni_md.h,jni.h三个文件
library_dirs=['/usr/lib64'], #这里如果把库文件移到工程中,也可以写成相对路径
libraries=['python3.6m'])]
setup(ext_modules=cythonize(extensions, language_level = 3))
当然也可以把库文件也放到我们的路径下,引用相对路径,但是要注意的是,因为库文件是经过编译的,所以在不同的系统下,或者在不同的指令集架构的计算机上是不能够通用的,这时候,就需要把你相对路径下的库换成所用计算机下的库。
(2)还要强调一点,这种调用方法是要依赖python环境的,在没有python环境的电脑上,即使你把python3.6m打到了工程里面也是不能正常运行的。
总结一下:这个过程用到的内容还是比较多的,C中调用java的方式,C中调用python的方式,cython相关的应用以及setup.py文件的使用等。打通这个流程,你会发现这几种语言间常用的交互流程基本都通了。
值得一提的是,通过将.pyx中的部分代码修改为C形式,可能会大大提高代码运行效率。