Java调用Python

本文最后更新于 2024年2月16日 下午

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

Why

  • 主要有下面两种原因:
    • 使用方式:例如采用Java开发的平台希望给用户提供易用的Python接口
    • 生态集成:例如将Java中的分布式能力和Python的AI生态相结合

How

  • 现如今有如下的解决方案:
    • IPC方案: 比如Py4J
      • 问题:因为涉及到进程间通信以及序列化,所以会有性能问题
    • Python运行在JVM的方案:比如Jython
      • 问题:因为CPython现在是主流,所以会有兼容性问题
    • FFI(Foreign Function Interface): 比如Pemja

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

Prepare

  • 首先需要安装以下环境,如遇到安装问题可以和ChatGPT聊一下~
    • 编程语言
      • Java: 1.8
      • Python: 3.9
      • C:c99
    • 编译与链接
      • gcc:动态链接

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

Learn By Doing

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

  1. 编写主体Java代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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文件
1
javac -h src/main/c/cn/syntomic/ffi/include src/main/java/cn/syntomic/ffi/FFIDemo.java
  • 观察生成的文件会发现我们需要实现一个C方法:
1
2
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函数实现
1
2
3
4
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]+d1w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1

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

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


  1. 将Python嵌入到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
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. 编译与运行
1
2
3
4
5
6
7
8
9
10
11
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/2023/02/25/Java调用Python/
作者
syntomic
发布于
2023年2月25日
更新于
2024年2月16日
许可协议