TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

9.1 KiB

Swoole AOT 编译器编程语言互调用机制实例:PHP 调用 JNI 接口实现调用任意 Java 类方法

概述

Swoole AOT 编译器是一个PHP的静态编译器,可以将PHP 项目或代码直接编译为二进制可执行文件。 AOT编译器基于ABI模式实现了PHPC/C++的互操作性,这使得PHP获得了与其他编程语言的直接调用能力,而不需要借助FFI或者编写PHP扩展。

本文将介绍如何使用JavaJNI接口,实现在 PHP 代码中直接调用 Java 类库 —— 创建 Java 对象、调用方法、读写字段,就像在 Java 代码中操作一样自然。

  1. 本实例程序的代码全部由 DeepSeek-4-Pro 生成,耗时约为50分钟
  2. GitHub: https://github.com/swoole/aot-compiler/tree/main/examples/jni

准备工作

  1. JDK(示例使用 OpenJDK 25,路径 /usr/lib/jvm/java-25-openjdk-amd64
  2. PHP 8.2+Swoole-Compiler 0.2.0
  3. 编译命令
swoole_compiler examples/jni/project.yml

核心接口

参见:php-src/jni.stub.php文件

<?php
function jni_init(string $classpath = "."): void {}
function jni_destroy(): void {}
function jni_find_class(string $className): mixed {}
function jni_find_method(mixed $objOrClass, string $methodName): mixed {}
function jni_find_field(mixed $objOrClass, string $fieldName): mixed {}
function jni_new_object(mixed $classHandle, array $args = []): mixed {}
function jni_call(mixed $objOrClass, mixed $method, array $args = []): mixed {}
function jni_get(mixed $objOrClass, mixed $field): mixed {}
function jni_set(mixed $objOrClass, mixed $field, mixed $value): void {}
  • 使用jni_init、jni_destroy 初始化 JVM 和销毁 JVM
  • 使用jni_find_class 查找 Java 类,返回 JniClass 句柄
  • 使用jni_find_method 查找 Java 方法,返回 JniMethod 句柄
  • 使用jni_find_field 查找 Java 类属性字段,返回 JniField 句柄
  • 使用jni_new_object 创建 Java 对象,返回 JniObject 句柄
  • 使用jni_call 调用 Java 方法,返回方法返回值
  • 使用jni_get 读取 Java 类属性字段,返回字段值
  • 使用jni_set 修改 Java 类属性字段,返回字段值

jni.cc代码中,会自定调用Java反射API获取类、方法、属性的类型, 并存储起来,在后续的调用中会根据反射信息,实现PHP类型与Java类型的自动转换。

类型转换

  • Java String → PHP string
  • Java int/long/short/byte → PHP int
  • Java float/double → PHP float
  • Java boolean → PHP bool
  • 其他 Java 对象 → JniObject 句柄
  • voidnull
PHP 类型 目标 Java 类型 转换说明
string java.lang.String 通过 NewStringUTF 创建 jstring
string 其他对象类型 自动转换为 jstring(可用于需要 CharSequence 等接口的参数)
int int / long / short / byte 直接转换为对应整数类型
int float / double 隐式转换为浮点数
float float / double 直接转换
bool boolean 直接转换
JniObject 对应的 Java 对象类型 提取原始 jobject 传递

第一步:编写 Java 类

在项目根目录(或 classpath 可访问的位置)创建一个 Java 类:

// Hello.java
public class Hello {
    private String name;
    private int age;

    public Hello(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String greet(String greeting) {
        return greeting + ", I'm " + name + ", " + age + " years old";
    }
}

编译为字节码:

javac Hello.java

除了调用自定义类之外,也可以调用Java标准库中的类,如 java.lang.StringBuilderjava.lang.String 等, 或者其他第三方类库,如 com.google.gson.Gsonorg.apache.commons.lang3.StringUtils 等,需要使用maven等包管理工具 引入,并使用 maven 构建项目。

第二步:配置项目

创建 project.yml,指定 JNI 头文件和 JVM 库的路径:

name: jni-example
version: 0.0.1
cxxflags: |
  -std=c++17
  -I/usr/lib/jvm/java-25-openjdk-amd64/include
  -I/usr/lib/jvm/java-25-openjdk-amd64/include/linux
  -Wall  
ldflags: |
  -L/usr/lib/jvm/java-25-openjdk-amd64/lib/server
  -ljvm
  -Wl,-rpath,/usr/lib/jvm/java-25-openjdk-amd64/lib/server  
sources:
  - php-src
  - ./cpp-src
  - main.php

说明

  • cxxflags-I 指向 JDKJNI 头文件目录
  • ldflags-ljvm 链接 JVM 动态库,-Wl,-rpath 确保运行时能找到 libjvm.so
  • sources./cpp-src 指向 C++ 实现目录(内含 jni.cc),php-src 指向 PHP stub 文件目录

第三步:编写 PHP 程序

<?php
// main.php
function main()
{
    // 1. 初始化 JVM,传入 classpath
    jni_init(".");
    echo "JVM initialized.\n";

    // --------------------------------------------------
    // 2. 操作自定义 Hello 类
    // --------------------------------------------------

    // 查找类 —— 返回 JniClass 句柄
    $helloClass = jni_find_class("Hello");

    // 查找方法和字段 —— 触发 Java 反射,返回缓存的 JniMethod / JniField 句柄
    $greet = jni_find_method($helloClass, "greet");
    $nameField = jni_find_field($helloClass, "name");
    $ageField = jni_find_field($helloClass, "age");

    // 创建对象 —— 构造器由参数数量自动匹配
    $hello = jni_new_object($helloClass, ["Swoole", 8]);

    // 调用方法
    $msg = jni_call($hello, $greet, ["你好"]);
    echo $msg . "\n";  // 你好, I'm Swoole, 8 years old

    // 读取字段
    $name = jni_get($hello, $nameField);  // "Swoole"
    $age  = jni_get($hello, $ageField);   // 8

    // 修改字段
    jni_set($hello, $nameField, "PHP");
    jni_set($hello, $ageField, 10);

    // 再次调用验证
    echo jni_call($hello, $greet, ["Hi"]) . "\n";
    // Hi, I'm PHP, 10 years old

    // --------------------------------------------------
    // 3. 操作标准 Java 类 (StringBuilder)
    // --------------------------------------------------

    $sbClass = jni_find_class("java.lang.StringBuilder");
    $append = jni_find_method($sbClass, "append");
    $toString = jni_find_method($sbClass, "toString");

    $sb = jni_new_object($sbClass, ["Hello"]);
    jni_call($sb, $append, [" Java"]);
    jni_call($sb, $append, [" JNI"]);

    $str = jni_call($sb, $toString, []);
    echo $str . "\n";  // Hello Java JNI

    // 4. 销毁 JVM
    jni_destroy();
}

第四步:编译运行

编译项目:

swoole_compiler examples/jni/project.yml

运行编译产物:

cd jni_example
LD_LIBRARY_PATH=/opt/php-8.4/lib:/path/to/phpx/lib:/usr/lib/jvm/java-25-openjdk-amd64/lib/server ./jni_example

请注意务必将Hello.class文件放在与jni_example相同的目录下。

执行结果:

swoole@swoole-26:~/workspace/aot/compiler$ ./jni_example 
=== Step 1: Initialize JVM ===
JVM initialized.

=== Step 2: Dynamic Java Object (Hello) ===
Found class: Hello
Created Hello object.
greet() → 你好, I'm Swoole, 8 years old
name = Swoole, age = 8
After set: name = PHP, age = 10
greet() → Hi, I'm PHP, 10 years old

=== Step 3: Standard Java Class (StringBuilder) ===
Found class: java.lang.StringBuilder
Created StringBuilder.
toString() → Hello Java JNI

=== Step 4: Destroy JVM ===
JVM destroyed.

内部原理

反射与缓存

当首次对某个类调用 jni_find_methodjni_find_field 时,C++ 层会触发惰性反射(lazy reflection)

  1. 通过 JNI 调用 Class.getDeclaredConstructors() 获取所有构造器
  2. 通过 JNI 调用 Class.getMethods() 获取所有 public 方法(含继承的)
  3. 通过 JNI 调用 Class.getDeclaredFields() 获取所有字段(含 private)
  4. 对每个方法/字段,获取其参数类型、返回类型,转换为 JNI 签名
  5. 调用 GetMethodID / GetFieldID 获取 JNI ID
  6. 全部信息缓存在 JniClass 内部的 C++ map 中

后续对同类的方法/字段查找直接命中缓存,无需再调 JNI

Box 类型体系

Box (phpx 基类)
 ├─ JniClass   — 包装 jclass 全局引用 + 反射缓存
 ├─ JniMethod  — 包装 jmethodID + 所有重载的签名信息
 ├─ JniField   — 包装 jfieldID + 字段类型签名 + 静态/实例标记
 └─ JniObject  — 包装 jobject 全局引用

每个 Box 子类在构造时设置 type_info 字段,使得 C++ 层无需 RTTI 即可安全地区分不同句柄类型。

限制与注意事项

  1. JVM 单实例:一个进程只能创建一个 JVM 实例,jni_init 不可重复调用
  2. classpath:运行目录需能访问到 .class 文件(通过 jni_init 的参数指定)
  3. 无序字段Java 反射不保证方法/字段的返回顺序,方法重载匹配不依赖顺序
  4. 资源释放JniClass / JniObject 的全局引用在 PHPX Box 析构时自动释放(DeleteGlobalRef),JVM 销毁时一并清理
  5. 线程安全:当前实现未考虑多线程场景(AOT 编译的程序默认为单线程模型)