先说句题外话:无论是在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)

但在过程中还是遇到了一些细节问题,在这篇文章的基础上整理了下面的内容。

一、Java代码,文件名Test.java

这一块儿和文档中介绍的稍有不同。新建一个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);
    }
}

二、Python文件,取名JavaTest.pyx(pyx时cython代码的后缀,python是cython的一个子集,因此里面的代码完全可以写成python)

我们的目的就是让这些接口在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

三、C文件,取名main.c

Java中直接调用的是C接口,而C接口封装了上面的Python接口。

//main.c
#include <jni.h>
#include <Python.h>
#include <stdio.h>

#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

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 <jni.h>
#include <Python.h>
#include <stdio.h>

#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形式,可能会大大提高代码运行效率。

更多推荐

Java调用Python