Java调用Python

每个编程语言都有其适用的范围,当人们需要结合不同的生态去完成一些功能时,就会遇到不同语言通信的问题,本篇文章我们结合一个具体示例,展示如何通过FFI语言交互接口实现在Java中调用Python。

Why

  • 主要有下面两种原因:

    • 使用方式:例如采用Java开发的平台希望给用户提供易用的Python接口

    • 生态集成:例如将Java中的分布式能力和Python的AI生态相结合

How

  • 现如今有如下的解决方案:

    • IPC方案: 比如Py4J

      • 问题:因为涉及到进程间通信以及序列化,所以会有性能问题

    • Python运行在JVM的方案:比如Jython

      • 问题:因为CPython现在是主流,所以会有兼容性问题

    • FFI(Foreign Function Interface): 比如Pemja

ffi.png

  • 本篇文章我们将给出一个示例说明如何使用FFI。

Prepare

  • 首先需要安装以下环境,如遇到安装问题可以和ChatGPT聊一下~

    • 编程语言

      • Java: 1.8

      • Python: 3.9

      • C:c99

    • 编译与链接

      • gcc:动态链接

由于笔者开发环境采用MacOS + x86平台,所以以下教程只对此平台有效,后续再补上其他环境的相应命令

Learn By Doing

目标:Java传入日期参数, 由Python返回星期几

  1. 编写主体Java代码

package cn.syntomic.ffi;

/** Foreign function interface demo */
public class FFIDemo {

    static {
        System.load(System.getenv("LIBPYTHON"));
        System.load(String.format("%s/FFIDemo.dylib", System.getProperty("user.dir")));
    }

    public static void main(String[] args) {
        System.out.println(new FFIDemo().dayOfWeek("1994-05-05"));
    }

    /**
     * 本地方法判断日期是星期几
     * @param date 日期
     */
    private native int dayOfWeek(String date);
}
  • 可以看出我们需要首先导入两个共享库

    • 运行Python所需的libpython库: 可以利用find-libpython得出

    • 本地方法实现后的动态FFIDemo库:之后会介绍如何编译生成

  1. 生成Header文件

javac -h src/main/c/cn/syntomic/ffi/include src/main/java/cn/syntomic/ffi/FFIDemo.java
  • 可见我们需要实现一个C方法

    JNIEXPORT jint JNICALL Java_cn_syntomic_ffi_FFIDemo_dayOfWeek
    (JNIEnv *, jobject, jstring);
    • 方法名: 由Java包名+类名+方法名组成

    • 方法参数

      • JNIEnv: 通过这个指针可以从运行的JVM中访问所需的类、对象、字段和方法

      • jobject: 方法所属于的Java对象

      • jstring: C JNI类型,详细对应可见JNI Types

  1. Python模块实现: 直接调用Python函数实现

from datetime import datetime

def day_of_week(date):
    return datetime.strptime(date, "%Y-%d-%m").weekday() + 1

一点数学:如果要直接去计算星期几,可以利用数论中Zeller公式

w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1

所以1994-05-05这个日期的就是星期四:

4=(94+[94/4]+[19/4]-2 * 19+[26 * (5+1)/10 ]+5-1) \mod 7

  1. 将Python嵌入到C中

JNIEXPORT jint JNICALL Java_cn_syntomic_ffi_FFIDemo_dayOfWeek
  (JNIEnv* env, jobject thisObject, jstring date) {
    int weekOfDay;

    // 初始化python解释器
    Py_Initialize();

    // 导入实现Python函数
    const char* pName = "day_of_week";
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./src/main/python/cn/syntomic/ffi')");
    PyObject* pModule = PyImport_Import(PyUnicode_FromString(pName));
    PyObject* pFunc = PyObject_GetAttrString(pModule, pName);

    // java类型转化为python参数
    PyObject* pArgs = PyTuple_New(1);
    PyTuple_SetItem(pArgs, 0, PyUnicode_FromString((*env)->GetStringUTFChars(env, date, NULL)));

    // 调用python函数
    PyObject* pValue = PyObject_CallObject(pFunc, pArgs);
    weekOfDay = PyLong_AsLong(pValue);

    // 关闭python解释器
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }

    return weekOfDay;
  }
  1. 编译与运行

export JAVA_HOME=${JAVA_HOME}
export PYTHONHOME=${PYTHONHOME}
export LIBPYTHON=${LIBPYTHON}

# 编译
gcc -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -I${PYTHONHOME}/include/python3.9 -I${PYTHONHOME}/include src/main/c/cn/syntomic/ffi/cn_syntomic_ffi_FFIDemo.c -o FFIDemo.o
# 生成动态链接库
gcc -dynamiclib -L${PYTHONHOME}/lib -lpython3.9 -ldl -o FFIDemo.dylib FFIDemo.o

javac -d target/classes/cn/syntomic/ffi src/main/java/cn/syntomic/ffi/FFIDemo.java
java -cp target/classes cn.syntomic.ffi.FFIDemo
  • 最终就会输出

4

Summary

本篇文章我们以一个示例展示如何使用FFI,详细代码参考ffi_demo, 深入研究的话可以参考Pemja

Refer

  1. Guide to JNI

  2. Embedding Python in Another Application

  3. 基于 FFI 的 PyFlink 下一代 Python 运行时介绍


Java调用Python
https://syntomic.cn/archives/java-call-python
作者
syntomic
发布于
2023年02月25日
更新于
2024年08月27日
许可协议