feat(jni): 添加JNI扩展功能支持Java与PHP交互

- 在.gitignore中添加*.class文件忽略规则
- 新增Hello.java示例类用于JNI测试
- 实现完整的JNI C++桥接代码,包括类型转换和异常处理
- 添加PHP stub文件定义JNI相关函数接口
- 创建main.php演示程序展示JNI功能使用方法
- 配置project
pull/1/head
韩天峰 3 months ago
parent d387667f1e
commit 8a27a0055b
  1. 3
      .gitignore
  2. 21
      examples/jni/Hello.java
  3. 263
      examples/jni/TUTORIAL.md
  4. 869
      examples/jni/cpp-src/jni.cc
  5. 76
      examples/jni/main.php
  6. 104
      examples/jni/php-src/jni.stub.php
  7. 15
      examples/jni/project.yml

3
.gitignore vendored

@ -14,4 +14,5 @@
*.dll
*.exe
*.obj
*.pdb
*.pdb
*.class

@ -0,0 +1,21 @@
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";
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}

@ -0,0 +1,263 @@
# Swoole AOT 编译器编程语言互调用机制实例:PHP 调用 JNI 接口实现调用任意 Java 类方法
## 概述
`Swoole AOT` 编译器是一个`PHP`的静态编译器,可以将`PHP` 项目或代码直接编译为二进制可执行文件。
`AOT`编译器基于`ABI`模式实现了`PHP`与`C/C++`的互操作性,这使得`PHP`获得了与其他编程语言的直接调用能力,而不需要借助`FFI`或者编写`PHP`扩展。
本文将介绍如何使用`Java`的`JNI`接口,实现在 `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. **编译命令**
```bash
swoole_compiler examples/jni/project.yml
```
## 核心接口
参见:`php-src/jni.stub.php`文件
```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` 句柄
- `void``null`
| 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` 类:
```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";
}
}
```
编译为字节码:
```bash
javac Hello.java
```
除了调用自定义类之外,也可以调用`Java`标准库中的类,如 `java.lang.StringBuilder`、`java.lang.String` 等,
或者其他第三方类库,如 `com.google.gson.Gson`、`org.apache.commons.lang3.StringUtils` 等,需要使用`maven`等包管理工具
引入,并使用 `maven` 构建项目。
## 第二步:配置项目
创建 `project.yml`,指定 `JNI` 头文件和 `JVM` 库的路径:
```yaml
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` 指向 `JDK``JNI` 头文件目录
> - `ldflags``-ljvm` 链接 `JVM` 动态库,`-Wl,-rpath` 确保运行时能找到 `libjvm.so`
> - `sources``./cpp-src` 指向 `C++` 实现目录(内含 `jni.cc`),`php-src` 指向 `PHP stub` 文件目录
## 第三步:编写 PHP 程序
```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();
}
```
## 第四步:编译运行
编译项目:
```bash
swoole_compiler examples/jni/project.yml
```
运行编译产物:
```bash
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`相同的目录下。
执行结果:
```bash
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_method``jni_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` 编译的程序默认为单线程模型)

@ -0,0 +1,869 @@
#include <phpx.h>
#include <jni.h>
#include <string>
#include <cstring>
#include <unordered_map>
#include <vector>
using namespace php;
static JavaVM *jvm = nullptr;
static JNIEnv *env = nullptr;
//----------------------------------------------------------------------
// JNI helper: check and rethrow JNI exceptions as PHP errors
//----------------------------------------------------------------------
static void check_jni_exception() {
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
throwError("JNI exception occurred");
}
}
//----------------------------------------------------------------------
// Cached reflection helpers (java.lang.reflect.* method IDs)
//----------------------------------------------------------------------
static jmethodID s_Class_getName = nullptr;
static jmethodID s_Class_getDeclaredConstructors = nullptr;
static jmethodID s_Class_getMethods = nullptr;
static jmethodID s_Class_getDeclaredFields = nullptr;
static jmethodID s_Constructor_getParameterTypes = nullptr;
static jmethodID s_Method_getName = nullptr;
static jmethodID s_Method_getParameterTypes = nullptr;
static jmethodID s_Method_getReturnType = nullptr;
static jmethodID s_Field_getName = nullptr;
static jmethodID s_Field_getType = nullptr;
static jmethodID s_Field_getModifiers = nullptr;
static bool s_reflection_inited = false;
static void init_reflection() {
if (s_reflection_inited) return;
jclass classCls = env->FindClass("java/lang/Class");
s_Class_getName = env->GetMethodID(classCls, "getName", "()Ljava/lang/String;");
s_Class_getDeclaredConstructors = env->GetMethodID(classCls, "getDeclaredConstructors", "()[Ljava/lang/reflect/Constructor;");
s_Class_getMethods = env->GetMethodID(classCls, "getMethods", "()[Ljava/lang/reflect/Method;");
s_Class_getDeclaredFields = env->GetMethodID(classCls, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
env->DeleteLocalRef(classCls);
jclass ctorCls = env->FindClass("java/lang/reflect/Constructor");
s_Constructor_getParameterTypes = env->GetMethodID(ctorCls, "getParameterTypes", "()[Ljava/lang/Class;");
env->DeleteLocalRef(ctorCls);
jclass methodCls = env->FindClass("java/lang/reflect/Method");
s_Method_getName = env->GetMethodID(methodCls, "getName", "()Ljava/lang/String;");
s_Method_getParameterTypes = env->GetMethodID(methodCls, "getParameterTypes", "()[Ljava/lang/Class;");
s_Method_getReturnType = env->GetMethodID(methodCls, "getReturnType", "()Ljava/lang/Class;");
env->DeleteLocalRef(methodCls);
jclass fieldCls = env->FindClass("java/lang/reflect/Field");
s_Field_getName = env->GetMethodID(fieldCls, "getName", "()Ljava/lang/String;");
s_Field_getType = env->GetMethodID(fieldCls, "getType", "()Ljava/lang/Class;");
s_Field_getModifiers = env->GetMethodID(fieldCls, "getModifiers", "()I");
env->DeleteLocalRef(fieldCls);
s_reflection_inited = true;
}
//----------------------------------------------------------------------
// Convert a java.lang.Class object to a JNI type signature string
//----------------------------------------------------------------------
static std::string class_to_jni_sig(jclass typeClass) {
jstring nameStr = (jstring) env->CallObjectMethod(typeClass, s_Class_getName);
const char *name = env->GetStringUTFChars(nameStr, nullptr);
std::string result(name);
env->ReleaseStringUTFChars(nameStr, name);
env->DeleteLocalRef(nameStr);
// Primitives
if (result == "int") return "I";
if (result == "long") return "J";
if (result == "float") return "F";
if (result == "double") return "D";
if (result == "boolean") return "Z";
if (result == "byte") return "B";
if (result == "char") return "C";
if (result == "short") return "S";
if (result == "void") return "V";
// Array types: Class.getName() returns "[Lfoo.Bar;" etc.
// Convert dots to slashes for object types inside arrays
for (auto &ch : result) {
if (ch == '.') ch = '/';
}
// Non-array object type: prepend L and append ;
if (result[0] != '[') {
result = "L" + result + ";";
}
return result;
}
//----------------------------------------------------------------------
// Count JNI type tags in an arg_tags string
//----------------------------------------------------------------------
static int count_tags(const std::string &arg_tags) {
int n = 0;
const char *p = arg_tags.c_str();
while (*p) {
if (*p == 'L') {
const char *semi = strchr(p, ';');
if (!semi) return -1;
p = semi + 1;
} else if (*p == '[') {
while (*p == '[') p++;
if (*p == 'L') {
const char *semi = strchr(p, ';');
if (!semi) return -1;
p = semi + 1;
} else {
p++;
}
} else {
p++;
}
n++;
}
return n;
}
//----------------------------------------------------------------------
// Step through JNI signature, optionally writing tag to out.
// If out is nullptr, just advances past the tag.
//----------------------------------------------------------------------
static const char *next_tag(const char *p, char *out = nullptr) {
if (*p == 'L') {
const char *semi = strchr(p, ';');
if (!semi) throwError("Invalid JNI object signature");
size_t n = semi - p + 1;
if (out) {
memcpy(out, p, n);
out[n] = '\0';
}
return semi + 1;
} else if (*p == '[') {
const char *q = p;
while (*q == '[') q++;
if (*q == 'L') {
const char *semi = strchr(q, ';');
if (!semi) throwError("Invalid JNI array signature");
size_t n = semi - p + 1;
if (out) {
memcpy(out, p, n);
out[n] = '\0';
}
return semi + 1;
} else {
if (out) {
out[0] = *p;
out[1] = '\0';
}
return p + 1;
}
} else {
if (out) {
out[0] = *p;
out[1] = '\0';
}
return p + 1;
}
}
//----------------------------------------------------------------------
// Data structures for cached reflection info
//----------------------------------------------------------------------
struct MethodInfo {
jmethodID method_id = nullptr;
std::string arg_tags; // e.g., "Ljava/lang/String;I"
std::string return_sig; // e.g., "Ljava/lang/String;"
};
struct FieldInfo {
jfieldID field_id = nullptr;
std::string sig; // e.g., "Ljava/lang/String;" or "I"
bool is_static = false;
};
//----------------------------------------------------------------------
// Box types: wrap JNI handles and cached reflection data
//----------------------------------------------------------------------
// Box type identifiers (stored in Box::type_info)
enum : uint32_t {
BOX_JNI_CLASS = 1,
BOX_JNI_METHOD = 2,
BOX_JNI_FIELD = 3,
BOX_JNI_OBJECT = 4,
};
class JniClass;
class JniClass : public Box {
public:
jclass cls;
std::unordered_map<std::string, std::vector<MethodInfo>> methods;
std::unordered_map<std::string, FieldInfo> fields;
std::vector<MethodInfo> constructors;
bool reflected = false;
explicit JniClass(jclass c) : cls(c) { type_info = BOX_JNI_CLASS; }
~JniClass() override {
if (cls && env) env->DeleteGlobalRef(cls);
}
void reflect();
const std::vector<MethodInfo> *find_method(const std::string &name);
const FieldInfo *find_field(const std::string &name);
const MethodInfo *find_constructor(int nargs);
};
class JniMethod : public Box {
public:
std::vector<MethodInfo> overloads;
JniMethod() { type_info = BOX_JNI_METHOD; }
// Score how well a PHP arg matches a JNI parameter type tag
static int score_arg_match(const Variant &arg, const char *tag_start) {
char tag[256];
next_tag(tag_start, tag);
switch (tag[0]) {
case 'L':
if (strcmp(tag, "Ljava/lang/String;") == 0 && arg.isString()) return 10;
if (arg.isString()) return 5; // auto-convert string → jstring
if (arg.isResource()) return 3; // JniObject
return 1;
case 'I': case 'J': case 'S': case 'B':
if (arg.isInt()) return 10;
if (arg.isFloat()) return 3;
return 1;
case 'F': case 'D':
if (arg.isFloat()) return 10;
if (arg.isInt()) return 5;
return 1;
case 'Z':
if (arg.isBool()) return 10;
return 1;
default:
return 1;
}
}
const MethodInfo *find_by_arg_count(int nargs, Array &args) const {
const MethodInfo *best = nullptr;
int best_score = -1;
for (auto &m : overloads) {
if (count_tags(m.arg_tags) != nargs) continue;
int score = 0;
const char *p = m.arg_tags.c_str();
for (int i = 0; i < nargs; i++) {
score += score_arg_match(args[i], p);
p = next_tag(p);
}
if (score > best_score) {
best_score = score;
best = &m;
}
}
return best;
}
};
class JniField : public Box {
public:
FieldInfo info;
JniField() { type_info = BOX_JNI_FIELD; }
};
class JniObject : public Box {
public:
jobject obj;
explicit JniObject(jobject o) : obj(o) { type_info = BOX_JNI_OBJECT; }
~JniObject() override {
if (obj && env) env->DeleteGlobalRef(obj);
}
};
//----------------------------------------------------------------------
// Lazy reflection: populate methods/fields/constructors via Java reflection
//----------------------------------------------------------------------
void JniClass::reflect() {
if (reflected) return;
init_reflection();
// --- Constructors via getDeclaredConstructors() ---
jobjectArray ctors = (jobjectArray) env->CallObjectMethod(cls, s_Class_getDeclaredConstructors);
jsize nctors = env->GetArrayLength(ctors);
for (jsize i = 0; i < nctors; i++) {
jobject ctor = env->GetObjectArrayElement(ctors, i);
jobjectArray paramTypes = (jobjectArray) env->CallObjectMethod(ctor, s_Constructor_getParameterTypes);
jsize nparams = env->GetArrayLength(paramTypes);
std::string argTags;
for (jsize j = 0; j < nparams; j++) {
jclass paramClass = (jclass) env->GetObjectArrayElement(paramTypes, j);
argTags += class_to_jni_sig(paramClass);
env->DeleteLocalRef(paramClass);
}
std::string fullSig = "(" + argTags + ")V";
jmethodID methodId = env->GetMethodID(cls, "<init>", fullSig.c_str());
constructors.push_back({methodId, argTags, "V"});
env->DeleteLocalRef(paramTypes);
env->DeleteLocalRef(ctor);
}
env->DeleteLocalRef(ctors);
// If no explicit constructors, add default no-arg constructor
if (nctors == 0) {
jmethodID methodId = env->GetMethodID(cls, "<init>", "()V");
if (!env->ExceptionCheck()) {
constructors.push_back({methodId, "", "V"});
} else {
env->ExceptionClear();
}
}
// --- Methods via getMethods() ---
jobjectArray methods_arr = (jobjectArray) env->CallObjectMethod(cls, s_Class_getMethods);
jsize nmethods = env->GetArrayLength(methods_arr);
for (jsize i = 0; i < nmethods; i++) {
jobject method = env->GetObjectArrayElement(methods_arr, i);
jstring nameStr = (jstring) env->CallObjectMethod(method, s_Method_getName);
const char *nameChars = env->GetStringUTFChars(nameStr, nullptr);
std::string methodName(nameChars);
env->ReleaseStringUTFChars(nameStr, nameChars);
env->DeleteLocalRef(nameStr);
// Parameter types
jobjectArray paramTypes = (jobjectArray) env->CallObjectMethod(method, s_Method_getParameterTypes);
jsize nparams = env->GetArrayLength(paramTypes);
std::string argTags;
for (jsize j = 0; j < nparams; j++) {
jclass paramClass = (jclass) env->GetObjectArrayElement(paramTypes, j);
argTags += class_to_jni_sig(paramClass);
env->DeleteLocalRef(paramClass);
}
// Return type
jclass returnClass = (jclass) env->CallObjectMethod(method, s_Method_getReturnType);
std::string returnSig = class_to_jni_sig(returnClass);
env->DeleteLocalRef(returnClass);
std::string fullSig = "(" + argTags + ")" + returnSig;
jmethodID methodId = env->GetMethodID(cls, methodName.c_str(), fullSig.c_str());
methods[methodName].push_back({methodId, argTags, returnSig});
env->DeleteLocalRef(paramTypes);
env->DeleteLocalRef(method);
}
env->DeleteLocalRef(methods_arr);
// --- Fields via getDeclaredFields() ---
jobjectArray fields_arr = (jobjectArray) env->CallObjectMethod(cls, s_Class_getDeclaredFields);
jsize nfields = env->GetArrayLength(fields_arr);
for (jsize i = 0; i < nfields; i++) {
jobject field = env->GetObjectArrayElement(fields_arr, i);
jstring nameStr = (jstring) env->CallObjectMethod(field, s_Field_getName);
const char *nameChars = env->GetStringUTFChars(nameStr, nullptr);
std::string fieldName(nameChars);
env->ReleaseStringUTFChars(nameStr, nameChars);
env->DeleteLocalRef(nameStr);
jclass typeClass = (jclass) env->CallObjectMethod(field, s_Field_getType);
std::string sig = class_to_jni_sig(typeClass);
env->DeleteLocalRef(typeClass);
jint mods = env->CallIntMethod(field, s_Field_getModifiers);
bool isStatic = (mods & 0x0008) != 0; // java.lang.reflect.Modifier.STATIC = 8
jfieldID fieldId;
if (isStatic) {
fieldId = env->GetStaticFieldID(cls, fieldName.c_str(), sig.c_str());
} else {
fieldId = env->GetFieldID(cls, fieldName.c_str(), sig.c_str());
}
fields[fieldName] = {fieldId, sig, isStatic};
env->DeleteLocalRef(field);
}
env->DeleteLocalRef(fields_arr);
reflected = true;
}
const std::vector<MethodInfo> *JniClass::find_method(const std::string &name) {
reflect();
auto it = methods.find(name);
return (it != methods.end()) ? &it->second : nullptr;
}
const FieldInfo *JniClass::find_field(const std::string &name) {
reflect();
auto it = fields.find(name);
return (it != fields.end()) ? &it->second : nullptr;
}
const MethodInfo *JniClass::find_constructor(int nargs) {
reflect();
for (auto &c : constructors) {
if (count_tags(c.arg_tags) == nargs) return &c;
}
return nullptr;
}
//----------------------------------------------------------------------
// Safe, non-throwing Box extraction with type checking via Box::getTypeInfo()
//----------------------------------------------------------------------
static Box *get_box(const Variant &v) {
if (!v.isResource()) return nullptr;
auto *mut = const_cast<Variant *>(&v);
auto res = Z_RES_P(mut->unwrap_ptr());
if (res->type != getBoxResourceId()) return nullptr;
return static_cast<Box *>(res->ptr);
}
template <uint32_t Type>
static inline bool is_box_type(const Variant &v) {
auto *box = get_box(v);
return box && box->getTypeInfo() == Type;
}
static inline JniClass *to_jni_class(const Variant &v) {
auto *box = get_box(v);
return (box && box->getTypeInfo() == BOX_JNI_CLASS) ? static_cast<JniClass *>(box) : nullptr;
}
static inline JniMethod *to_jni_method(const Variant &v) {
auto *box = get_box(v);
return (box && box->getTypeInfo() == BOX_JNI_METHOD) ? static_cast<JniMethod *>(box) : nullptr;
}
static inline JniField *to_jni_field(const Variant &v) {
auto *box = get_box(v);
return (box && box->getTypeInfo() == BOX_JNI_FIELD) ? static_cast<JniField *>(box) : nullptr;
}
static inline JniObject *to_jni_object(const Variant &v) {
auto *box = get_box(v);
return (box && box->getTypeInfo() == BOX_JNI_OBJECT) ? static_cast<JniObject *>(box) : nullptr;
}
//----------------------------------------------------------------------
// Extract jclass from a Variant (JniClass or JniObject)
//----------------------------------------------------------------------
static jclass resolve_class(const Variant &v) {
if (auto *jc = to_jni_class(v)) {
return jc->cls;
}
if (auto *jo = to_jni_object(v)) {
return env->GetObjectClass(jo->obj);
}
throwError("Argument must be a JniClass or JniObject");
return nullptr;
}
// Get the JniClass* from a Variant (JniClass directly, or from JniObject's class)
static JniClass *resolve_jni_class(const Variant &v) {
if (auto *jc = to_jni_class(v)) {
return jc;
}
throwError("Argument must be a JniClass handle");
return nullptr;
}
static jobject resolve_object(const Variant &v) {
if (auto *jo = to_jni_object(v)) {
return jo->obj;
}
throwError("Argument must be a JniObject");
return nullptr;
}
static bool is_jni_class(const Variant &v) {
return is_box_type<BOX_JNI_CLASS>(v);
}
//----------------------------------------------------------------------
// Marshal: Variant → jvalue (by single-char JNI type tag)
//----------------------------------------------------------------------
static jvalue marshal_arg(const Variant &arg, char tag) {
jvalue jv{};
switch (tag) {
case 'Z': jv.z = (jboolean) arg.toBool(); break;
case 'B': jv.b = (jbyte) arg.toInt(); break;
case 'C': jv.c = (jchar) arg.toInt(); break;
case 'S': jv.s = (jshort) arg.toInt(); break;
case 'I': jv.i = (jint) arg.toInt(); break;
case 'J': jv.j = (jlong) arg.toInt(); break;
case 'F': jv.f = (jfloat) arg.toFloat(); break;
case 'D': jv.d = (jdouble) arg.toFloat(); break;
case 'L':
if (auto *jo = to_jni_object(arg)) {
jv.l = jo->obj;
} else {
jv.l = env->NewStringUTF(arg.toCString());
}
break;
case '[':
if (auto *jo = to_jni_object(arg)) {
jv.l = jo->obj;
} else {
throwError("Array argument must be a JniObject");
}
break;
default:
throwError("Unknown JNI tag: %c", tag);
}
return jv;
}
//----------------------------------------------------------------------
// Marshal: JNI return → Variant (by full type signature string)
//----------------------------------------------------------------------
static Variant marshal_return(jvalue jv, const std::string &return_sig) {
const char *sig = return_sig.c_str();
switch (sig[0]) {
case 'V': return nullptr;
case 'Z': return Variant((bool) jv.z);
case 'B': return Variant((Int) jv.b);
case 'C': return Variant((Int) jv.c);
case 'S': return Variant((Int) jv.s);
case 'I': return Variant((Int) jv.i);
case 'J': return Variant((Int) jv.j);
case 'F': return Variant((double) jv.f);
case 'D': return Variant(jv.d);
case 'L':
case '[': {
if (!jv.l) return nullptr;
jclass stringClass = env->FindClass("java/lang/String");
if (env->IsInstanceOf((jobject) jv.l, stringClass)) {
auto *jstr = (jstring) jv.l;
const char *chars = env->GetStringUTFChars(jstr, nullptr);
String ret(chars);
env->ReleaseStringUTFChars(jstr, chars);
env->DeleteLocalRef(stringClass);
return ret;
}
env->DeleteLocalRef(stringClass);
jobject global = env->NewGlobalRef((jobject) jv.l);
return {new JniObject(global)};
}
default:
throwError("Unknown JNI return type: %s", sig);
return {};
}
}
//----------------------------------------------------------------------
// PHP-callable functions
//----------------------------------------------------------------------
void php_jni_init(String classpath) {
if (jvm) return;
JavaVMInitArgs vm_args;
JavaVMOption options[2];
std::string cp = "-Djava.class.path=" + std::string(classpath.data(), classpath.length());
options[0].optionString = cp.data();
options[1].optionString = const_cast<char *>("-Xcheck:jni");
vm_args.version = JNI_VERSION_10;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
jint rc = JNI_CreateJavaVM(&jvm, (void **) &env, &vm_args);
if (rc != JNI_OK) {
throwError("Failed to create JVM, error code: %d", rc);
}
}
void php_jni_destroy() {
if (jvm) {
jvm->DestroyJavaVM();
jvm = nullptr;
env = nullptr;
}
}
/**
* jni_find_class(string $className): mixed
* Returns a JniClass handle. Reflection is lazy, triggered on first use.
*/
var php_jni_find_class(String className) {
if (!jvm) throwError("JNI not initialized, call jni_init() first");
std::string cn(className.data(), className.length());
for (auto &ch : cn) {
if (ch == '.') ch = '/';
}
jclass cls = env->FindClass(cn.c_str());
check_jni_exception();
if (!cls) {
throwError("Class not found: %s", className.data());
return {};
}
jclass globalCls = (jclass) env->NewGlobalRef(cls);
env->DeleteLocalRef(cls);
return {new JniClass(globalCls)};
}
/**
* jni_find_method(mixed $objOrClass, string $methodName): mixed
* Returns a JniMethod handle containing all overloads of the named method.
*/
var php_jni_find_method(var objOrClass, String methodName) {
auto *jc = resolve_jni_class(objOrClass);
auto *overloads = jc->find_method(std::string(methodName.data(), methodName.length()));
if (!overloads || overloads->empty()) {
throwError("Method not found: %s", methodName.data());
return {};
}
auto *box = new JniMethod();
box->overloads = *overloads;
return {box};
}
/**
* jni_find_field(mixed $objOrClass, string $fieldName): mixed
* Returns a JniField handle with cached type and field ID.
*/
var php_jni_find_field(var objOrClass, String fieldName) {
auto *jc = resolve_jni_class(objOrClass);
auto *info = jc->find_field(std::string(fieldName.data(), fieldName.length()));
if (!info) {
throwError("Field not found: %s", fieldName.data());
return {};
}
auto *box = new JniField();
box->info = *info;
return {box};
}
/**
* jni_new_object(mixed $classHandle, array $args = []): mixed
* Creates a new Java object. Constructor is found automatically by arg count.
*/
var php_jni_new_object(var classHandle, Array args) {
auto *jc = to_jni_class(classHandle);
if (!jc) throwError("First argument must be a JniClass handle");
int nargs = args.count();
auto *ctorInfo = jc->find_constructor(nargs);
if (!ctorInfo) {
throwError("No constructor found with %d argument(s)", nargs);
return {};
}
// Marshal args using cached signature
std::vector<jvalue> jvals(nargs);
const char *p = ctorInfo->arg_tags.c_str();
for (int i = 0; i < nargs; i++) {
char tag[256];
p = next_tag(p, tag);
jvals[i] = marshal_arg(args[i], tag[0]);
}
jobject obj = env->NewObjectA(jc->cls, ctorInfo->method_id, jvals.data());
check_jni_exception();
if (!obj) {
throwError("Failed to create object");
return {};
}
jobject globalObj = env->NewGlobalRef(obj);
env->DeleteLocalRef(obj);
return {new JniObject(globalObj)};
}
/**
* jni_call(mixed $objOrClass, mixed $method, array $args = []): mixed
* Call an instance or static method. $method is a JniMethod handle.
*/
var php_jni_call(var objOrClass, var method, Array args) {
jclass cls = resolve_class(objOrClass);
jobject obj = is_jni_class(objOrClass) ? nullptr : resolve_object(objOrClass);
auto *jm = to_jni_method(method);
if (!jm) throwError("Second argument must be a JniMethod handle");
int nargs = args.count();
auto *methodInfo = jm->find_by_arg_count(nargs, args);
if (!methodInfo) {
throwError("No matching overload of method with %d argument(s)", nargs);
return {};
}
// Marshal args
std::vector<jvalue> jvals(nargs);
const char *p = methodInfo->arg_tags.c_str();
for (int i = 0; i < nargs; i++) {
char tag[256];
p = next_tag(p, tag);
jvals[i] = marshal_arg(args[i], tag[0]);
}
bool isStatic = (obj == nullptr);
jvalue ret{};
const char *retSig = methodInfo->return_sig.c_str();
switch (retSig[0]) {
case 'V':
if (isStatic) {
env->CallStaticVoidMethodA(cls, methodInfo->method_id, jvals.data());
} else {
env->CallVoidMethodA(obj, methodInfo->method_id, jvals.data());
}
check_jni_exception();
if (!isStatic) env->DeleteLocalRef(cls);
return nullptr;
case 'Z':
ret.z = isStatic ? env->CallStaticBooleanMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallBooleanMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'B':
ret.b = isStatic ? env->CallStaticByteMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallByteMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'C':
ret.c = isStatic ? env->CallStaticCharMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallCharMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'S':
ret.s = isStatic ? env->CallStaticShortMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallShortMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'I':
ret.i = isStatic ? env->CallStaticIntMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallIntMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'J':
ret.j = isStatic ? env->CallStaticLongMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallLongMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'F':
ret.f = isStatic ? env->CallStaticFloatMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallFloatMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'D':
ret.d = isStatic ? env->CallStaticDoubleMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallDoubleMethodA(obj, methodInfo->method_id, jvals.data());
break;
case 'L':
case '[':
ret.l = isStatic ? env->CallStaticObjectMethodA(cls, methodInfo->method_id, jvals.data())
: env->CallObjectMethodA(obj, methodInfo->method_id, jvals.data());
break;
default:
if (!isStatic) env->DeleteLocalRef(cls);
throwError("Unknown JNI return type: %s", retSig);
return {};
}
check_jni_exception();
if (!isStatic) {
env->DeleteLocalRef(cls);
}
return marshal_return(ret, methodInfo->return_sig);
}
/**
* jni_get(mixed $objOrClass, mixed $field): mixed
* Read an instance or static field. $field is a JniField handle.
*/
var php_jni_get(var objOrClass, var field) {
jclass cls = resolve_class(objOrClass);
jobject obj = is_jni_class(objOrClass) ? nullptr : resolve_object(objOrClass);
auto *jf = to_jni_field(field);
if (!jf) throwError("Field argument must be a JniField handle");
jvalue jv{};
const char *s = jf->info.sig.c_str();
if (jf->info.is_static) {
switch (s[0]) {
case 'Z': jv.z = env->GetStaticBooleanField(cls, jf->info.field_id); break;
case 'B': jv.b = env->GetStaticByteField(cls, jf->info.field_id); break;
case 'C': jv.c = env->GetStaticCharField(cls, jf->info.field_id); break;
case 'S': jv.s = env->GetStaticShortField(cls, jf->info.field_id); break;
case 'I': jv.i = env->GetStaticIntField(cls, jf->info.field_id); break;
case 'J': jv.j = env->GetStaticLongField(cls, jf->info.field_id); break;
case 'F': jv.f = env->GetStaticFloatField(cls, jf->info.field_id); break;
case 'D': jv.d = env->GetStaticDoubleField(cls, jf->info.field_id); break;
case 'L': case '[': jv.l = env->GetStaticObjectField(cls, jf->info.field_id); break;
default: throwError("Unknown field type: %s", s);
}
} else {
switch (s[0]) {
case 'Z': jv.z = env->GetBooleanField(obj, jf->info.field_id); break;
case 'B': jv.b = env->GetByteField(obj, jf->info.field_id); break;
case 'C': jv.c = env->GetCharField(obj, jf->info.field_id); break;
case 'S': jv.s = env->GetShortField(obj, jf->info.field_id); break;
case 'I': jv.i = env->GetIntField(obj, jf->info.field_id); break;
case 'J': jv.j = env->GetLongField(obj, jf->info.field_id); break;
case 'F': jv.f = env->GetFloatField(obj, jf->info.field_id); break;
case 'D': jv.d = env->GetDoubleField(obj, jf->info.field_id); break;
case 'L': case '[': jv.l = env->GetObjectField(obj, jf->info.field_id); break;
default: throwError("Unknown field type: %s", s);
}
}
check_jni_exception();
if (!is_jni_class(objOrClass)) {
env->DeleteLocalRef(cls);
}
return marshal_return(jv, jf->info.sig);
}
/**
* jni_set(mixed $objOrClass, mixed $field, mixed $value): void
* Write an instance or static field. $field is a JniField handle.
*/
void php_jni_set(var objOrClass, var field, var value) {
jclass cls = resolve_class(objOrClass);
jobject obj = is_jni_class(objOrClass) ? nullptr : resolve_object(objOrClass);
auto *jf = to_jni_field(field);
if (!jf) throwError("Field argument must be a JniField handle");
jvalue jv = marshal_arg(value, jf->info.sig[0]);
const char *s = jf->info.sig.c_str();
if (jf->info.is_static) {
switch (s[0]) {
case 'Z': env->SetStaticBooleanField(cls, jf->info.field_id, jv.z); break;
case 'B': env->SetStaticByteField(cls, jf->info.field_id, jv.b); break;
case 'C': env->SetStaticCharField(cls, jf->info.field_id, jv.c); break;
case 'S': env->SetStaticShortField(cls, jf->info.field_id, jv.s); break;
case 'I': env->SetStaticIntField(cls, jf->info.field_id, jv.i); break;
case 'J': env->SetStaticLongField(cls, jf->info.field_id, jv.j); break;
case 'F': env->SetStaticFloatField(cls, jf->info.field_id, jv.f); break;
case 'D': env->SetStaticDoubleField(cls, jf->info.field_id, jv.d); break;
case 'L': case '[': env->SetStaticObjectField(cls, jf->info.field_id, (jobject) jv.l); break;
default: throwError("Unknown field type: %s", s);
}
} else {
switch (s[0]) {
case 'Z': env->SetBooleanField(obj, jf->info.field_id, jv.z); break;
case 'B': env->SetByteField(obj, jf->info.field_id, jv.b); break;
case 'C': env->SetCharField(obj, jf->info.field_id, jv.c); break;
case 'S': env->SetShortField(obj, jf->info.field_id, jv.s); break;
case 'I': env->SetIntField(obj, jf->info.field_id, jv.i); break;
case 'J': env->SetLongField(obj, jf->info.field_id, jv.j); break;
case 'F': env->SetFloatField(obj, jf->info.field_id, jv.f); break;
case 'D': env->SetDoubleField(obj, jf->info.field_id, jv.d); break;
case 'L': case '[': env->SetObjectField(obj, jf->info.field_id, (jobject) jv.l); break;
default: throwError("Unknown field type: %s", s);
}
}
check_jni_exception();
if (!is_jni_class(objOrClass)) {
env->DeleteLocalRef(cls);
}
}

@ -0,0 +1,76 @@
<?php
function main()
{
// 1. 初始化 JVM
echo "=== Step 1: Initialize JVM ===\n";
jni_init(".");
echo "JVM initialized.\n\n";
// ---------------------------------------------------------------
// 2. 动态创建自定义 Hello 对象、调用方法、读写属性
// ---------------------------------------------------------------
echo "=== Step 2: Dynamic Java Object (Hello) ===\n";
// 查找类
$helloClass = jni_find_class("Hello");
echo "Found class: Hello\n";
// 获取方法、属性(反射 + 缓存为 Box 类型)
$greet = jni_find_method($helloClass, "greet");
$nameField = jni_find_field($helloClass, "name");
$ageField = jni_find_field($helloClass, "age");
// 创建对象: Hello(String name, int age)
$hello = jni_new_object($helloClass, ["Swoole", 8]);
echo "Created Hello object.\n";
// 调用方法: String greet(String greeting)
$msg = jni_call($hello, $greet, ["你好"]);
echo "greet() → " . $msg . "\n";
// 读取属性
$name = jni_get($hello, $nameField);
$age = jni_get($hello, $ageField);
echo "name = " . $name . ", age = " . $age . "\n";
// 修改属性
jni_set($hello, $nameField, "PHP");
jni_set($hello, $ageField, 10);
$name2 = jni_get($hello, $nameField);
$age2 = jni_get($hello, $ageField);
echo "After set: name = " . $name2 . ", age = " . $age2 . "\n";
echo "greet() → " . jni_call($hello, $greet, ["Hi"]) . "\n\n";
// ---------------------------------------------------------------
// 3. 使用标准 Java 类 (StringBuilder)
// ---------------------------------------------------------------
echo "=== Step 3: Standard Java Class (StringBuilder) ===\n";
$sbClass = jni_find_class("java.lang.StringBuilder");
echo "Found class: java.lang.StringBuilder\n";
// 获取方法
$append = jni_find_method($sbClass, "append");
$toString = jni_find_method($sbClass, "toString");
// StringBuilder sb = new StringBuilder("Hello");
$sb = jni_new_object($sbClass, ["Hello"]);
echo "Created StringBuilder.\n";
// sb.append(" Java")
$sb2 = jni_call($sb, $append, [" Java"]);
// sb.append(" JNI")
jni_call($sb, $append, [" JNI"]);
// sb.toString()
$str = jni_call($sb, $toString, []);
echo "toString() → " . $str . "\n\n";
// ---------------------------------------------------------------
// 4. 销毁 JVM
// ---------------------------------------------------------------
echo "=== Step 4: Destroy JVM ===\n";
jni_destroy();
echo "JVM destroyed.\n";
}

@ -0,0 +1,104 @@
<?php
/**
* Initialize the JVM with a classpath.
* Must be called once before any other jni_* functions.
*/
function jni_init(string $classpath = "."): void
{
}
/**
* Destroy the JVM and release all resources.
*/
function jni_destroy(): void
{
}
/**
* Find a Java class by name (e.g. "Hello" or "java.lang.StringBuilder").
* Returns a JniClass handle.
*/
function jni_find_class(string $className): mixed
{
}
/**
* Find a method by name. Returns a JniMethod handle that caches
* the jmethodID and type signatures. Handles method overloading;
* the correct overload is selected at call time by argument count.
*
* @param mixed $objOrClass JniClass handle (or JniObject to get its class)
* @param string $methodName Java method name
* @return mixed JniMethod handle
*/
function jni_find_method(mixed $objOrClass, string $methodName): mixed
{
}
/**
* Find a field by name. Returns a JniField handle that caches
* the jfieldID and type signature.
*
* @param mixed $objOrClass JniClass handle (or JniObject to get its class)
* @param string $fieldName Java field name
* @return mixed JniField handle
*/
function jni_find_field(mixed $objOrClass, string $fieldName): mixed
{
}
/**
* Create a new Java object. The constructor is found automatically
* by matching argument count.
*
* @param mixed $classHandle A JniClass handle from jni_find_class()
* @param array $args Constructor arguments
* @return mixed JniObject handle
*/
function jni_new_object(mixed $classHandle, array $args = []): mixed
{
}
/**
* Call a Java method (instance or static).
*
* @param mixed $objOrClass JniObject (instance method) or JniClass (static method)
* @param mixed $method JniMethod handle from jni_find_method()
* @param array $args Method arguments
* @return mixed Return value (string/int/bool/JniObject, or null for void)
*/
function jni_call(mixed $objOrClass, mixed $method, array $args = []): mixed
{
}
/**
* Read a Java field (instance or static).
*
* @param mixed $objOrClass JniObject (instance field) or JniClass (static field)
* @param mixed $field JniField handle from jni_find_field()
* @return mixed Field value
*/
function jni_get(mixed $objOrClass, mixed $field): mixed
{
}
/**
* Write a Java field (instance or static).
*
* @param mixed $objOrClass JniObject (instance field) or JniClass (static field)
* @param mixed $field JniField handle from jni_find_field()
* @param mixed $value New value to assign
*/
function jni_set(mixed $objOrClass, mixed $field, mixed $value): void
{
}

@ -0,0 +1,15 @@
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
Loading…
Cancel
Save