diff --git a/.gitignore b/.gitignore index 7bc5ef81..86e3ca0e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ *.dll *.exe *.obj -*.pdb \ No newline at end of file +*.pdb +*.class \ No newline at end of file diff --git a/examples/jni/Hello.java b/examples/jni/Hello.java new file mode 100644 index 00000000..ac33c57b --- /dev/null +++ b/examples/jni/Hello.java @@ -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; + } +} diff --git a/examples/jni/TUTORIAL.md b/examples/jni/TUTORIAL.md new file mode 100644 index 00000000..52dfe38d --- /dev/null +++ b/examples/jni/TUTORIAL.md @@ -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: + +## 准备工作 + +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 + **说明**: +> - `cxxflags` 中 `-I` 指向 `JDK` 的 `JNI` 头文件目录 +> - `ldflags` 中 `-ljvm` 链接 `JVM` 动态库,`-Wl,-rpath` 确保运行时能找到 `libjvm.so` +> - `sources` 中 `./cpp-src` 指向 `C++` 实现目录(内含 `jni.cc`),`php-src` 指向 `PHP stub` 文件目录 + +## 第三步:编写 PHP 程序 + +```php + 请注意务必将`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` 编译的程序默认为单线程模型) diff --git a/examples/jni/cpp-src/jni.cc b/examples/jni/cpp-src/jni.cc new file mode 100644 index 00000000..8b849508 --- /dev/null +++ b/examples/jni/cpp-src/jni.cc @@ -0,0 +1,869 @@ +#include +#include +#include +#include +#include +#include + +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> methods; + std::unordered_map fields; + std::vector 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 *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 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, "", 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, "", "()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 *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(&v); + auto res = Z_RES_P(mut->unwrap_ptr()); + if (res->type != getBoxResourceId()) return nullptr; + return static_cast(res->ptr); +} + +template +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(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(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(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(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(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("-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 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 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); + } +} diff --git a/examples/jni/main.php b/examples/jni/main.php new file mode 100644 index 00000000..f83264db --- /dev/null +++ b/examples/jni/main.php @@ -0,0 +1,76 @@ +