Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
304 lines (274 loc) · 10.6 KB Raw
// A thin JNI layer.
//
// Inside the process we do not have to decode HotSpot's heap by hand: the JVM
// exposes a proper API. `JNI_GetCreatedJavaVMs` hands us the VM that is already
// running, and because our hook executes on the game's render thread — itself a
// Java thread — `GetEnv` gives us a usable environment with no attaching.
//
// Two rules keep this safe, and every helper below follows them:
//
//   * a pending exception makes the *next* JNI call undefined behaviour, so
//     every call that can throw is checked and cleared immediately;
//   * every frame runs inside a local reference frame, so the references made
//     while drawing are released together instead of leaking the heap away.
#pragma once

#include <windows.h>

#include <jni.h>

#include <initializer_list>
#include <string>

namespace lodestone::jni {

/// The JVM already running in this process, or null if there isn't one.
inline JavaVM* java_vm() {
    HMODULE jvm = GetModuleHandleA("jvm.dll");
    if (!jvm) return nullptr;
    using PFNGetCreatedJavaVMs = jint(JNICALL*)(JavaVM**, jsize, jsize*);
    auto f = reinterpret_cast<PFNGetCreatedJavaVMs>(
        reinterpret_cast<void*>(GetProcAddress(jvm, "JNI_GetCreatedJavaVMs")));
    if (!f) return nullptr;
    JavaVM* vm = nullptr;
    jsize count = 0;
    if (f(&vm, 1, &count) != JNI_OK || count < 1) return nullptr;
    return vm;
}

/// Convenience constructors for the argument union.
inline jvalue jv(jobject v) { jvalue a{}; a.l = v; return a; }
inline jvalue jv(jint v) { jvalue a{}; a.i = v; return a; }
inline jvalue jv(jfloat v) { jvalue a{}; a.f = v; return a; }
inline jvalue jv(jdouble v) { jvalue a{}; a.d = v; return a; }
inline jvalue jv(jlong v) { jvalue a{}; a.j = v; return a; }
inline jvalue jv(bool v) { jvalue a{}; a.z = v ? JNI_TRUE : JNI_FALSE; return a; }

using Args = std::initializer_list<jvalue>;

class Env {
  public:
    JNIEnv* env = nullptr;

    Env() = default;
    explicit Env(JNIEnv* e) : env(e) {}

    /// An environment for the calling thread, if it is already a Java thread.
    /// The render thread is, which is exactly where our hook runs.
    static Env current(JavaVM* vm) {
        if (!vm) return {};
        void* p = nullptr;
        if (vm->GetEnv(&p, JNI_VERSION_1_6) != JNI_OK) return {};
        return Env(static_cast<JNIEnv*>(p));
    }

    explicit operator bool() const { return env != nullptr; }

    /// Clear a pending exception, reporting whether there was one. Called after
    /// everything that can throw.
    bool failed() const {
        if (env->ExceptionCheck()) {
            env->ExceptionClear();
            return true;
        }
        return false;
    }

    // ---- lookup ----------------------------------------------------------

    /// A class by JVM name ("net/minecraft/client/Minecraft"), as a *global*
    /// reference so it stays valid past this frame.
    jclass find_class(const char* name) const {
        jclass local = env->FindClass(name);
        if (failed() || !local) return nullptr;
        auto g = static_cast<jclass>(env->NewGlobalRef(local));
        env->DeleteLocalRef(local);
        return g;
    }

    jmethodID method(jclass cls, const char* name, const char* sig) const {
        if (!cls) return nullptr;
        jmethodID id = env->GetMethodID(cls, name, sig);
        if (failed()) return nullptr;
        return id;
    }

    jmethodID static_method(jclass cls, const char* name, const char* sig) const {
        if (!cls) return nullptr;
        jmethodID id = env->GetStaticMethodID(cls, name, sig);
        if (failed()) return nullptr;
        return id;
    }

    jfieldID field(jclass cls, const char* name, const char* sig) const {
        if (!cls) return nullptr;
        jfieldID id = env->GetFieldID(cls, name, sig);
        if (failed()) return nullptr;
        return id;
    }

    jfieldID static_field(jclass cls, const char* name, const char* sig) const {
        if (!cls) return nullptr;
        jfieldID id = env->GetStaticFieldID(cls, name, sig);
        if (failed()) return nullptr;
        return id;
    }

    // ---- calling ---------------------------------------------------------

    jobject call_obj(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return nullptr;
        jobject r = env->CallObjectMethodA(obj, id, args.begin());
        if (failed()) return nullptr;
        return r;
    }
    jint call_int(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return 0;
        jint r = env->CallIntMethodA(obj, id, args.begin());
        if (failed()) return 0;
        return r;
    }
    jfloat call_float(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return 0.0f;
        jfloat r = env->CallFloatMethodA(obj, id, args.begin());
        if (failed()) return 0.0f;
        return r;
    }
    jdouble call_double(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return 0.0;
        jdouble r = env->CallDoubleMethodA(obj, id, args.begin());
        if (failed()) return 0.0;
        return r;
    }
    jlong call_long(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return 0;
        jlong r = env->CallLongMethodA(obj, id, args.begin());
        if (failed()) return 0;
        return r;
    }
    bool call_bool(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return false;
        jboolean r = env->CallBooleanMethodA(obj, id, args.begin());
        if (failed()) return false;
        return r == JNI_TRUE;
    }
    /// Call a method non-virtually, as `super.foo()` would. Needed for methods
    /// that are private on the declaring class: an ordinary virtual call looks
    /// the name up on the object's real class and finds the wrong one (or
    /// nothing). `cls` must be the class the method id came from.
    bool call_nonvirtual_bool(jobject obj, jclass cls, jmethodID id, Args args = {}) const {
        if (!obj || !cls || !id) return false;
        jboolean r = env->CallNonvirtualBooleanMethodA(obj, cls, id, args.begin());
        if (failed()) return false;
        return r == JNI_TRUE;
    }
    void call_void(jobject obj, jmethodID id, Args args = {}) const {
        if (!obj || !id) return;
        env->CallVoidMethodA(obj, id, args.begin());
        failed();
    }
    /// Construct a Java object. The constructor id comes from `method(cls,
    /// "<init>", sig)`.
    jobject new_object(jclass cls, jmethodID ctor, Args args = {}) const {
        if (!cls || !ctor) return nullptr;
        jobject o = env->NewObjectA(cls, ctor, args.begin());
        if (failed()) return nullptr;
        return o;
    }

    jobject call_static_obj(jclass cls, jmethodID id, Args args = {}) const {
        if (!cls || !id) return nullptr;
        jobject r = env->CallStaticObjectMethodA(cls, id, args.begin());
        if (failed()) return nullptr;
        return r;
    }
    jint call_static_int(jclass cls, jmethodID id, Args args = {}) const {
        if (!cls || !id) return 0;
        jint r = env->CallStaticIntMethodA(cls, id, args.begin());
        if (failed()) return 0;
        return r;
    }

    // ---- fields ----------------------------------------------------------

    jobject obj_field(jobject obj, jfieldID id) const {
        if (!obj || !id) return nullptr;
        jobject r = env->GetObjectField(obj, id);
        if (failed()) return nullptr;
        return r;
    }
    jint int_field(jobject obj, jfieldID id) const {
        if (!obj || !id) return 0;
        jint r = env->GetIntField(obj, id);
        if (failed()) return 0;
        return r;
    }
    jfloat float_field(jobject obj, jfieldID id) const {
        if (!obj || !id) return 0.0f;
        jfloat r = env->GetFloatField(obj, id);
        if (failed()) return 0.0f;
        return r;
    }
    jdouble double_field(jobject obj, jfieldID id) const {
        if (!obj || !id) return 0.0;
        jdouble r = env->GetDoubleField(obj, id);
        if (failed()) return 0.0;
        return r;
    }
    bool bool_field(jobject obj, jfieldID id) const {
        if (!obj || !id) return false;
        jboolean r = env->GetBooleanField(obj, id);
        if (failed()) return false;
        return r == JNI_TRUE;
    }
    jobject static_obj_field(jclass cls, jfieldID id) const {
        if (!cls || !id) return nullptr;
        jobject r = env->GetStaticObjectField(cls, id);
        if (failed()) return nullptr;
        return r;
    }

    void set_float(jobject obj, jfieldID id, jfloat v) const {
        if (!obj || !id) return;
        env->SetFloatField(obj, id, v);
        failed();
    }
    void set_double(jobject obj, jfieldID id, jdouble v) const {
        if (!obj || !id) return;
        env->SetDoubleField(obj, id, v);
        failed();
    }
    void set_int(jobject obj, jfieldID id, jint v) const {
        if (!obj || !id) return;
        env->SetIntField(obj, id, v);
        failed();
    }
    void set_bool(jobject obj, jfieldID id, bool v) const {
        if (!obj || !id) return;
        env->SetBooleanField(obj, id, v ? JNI_TRUE : JNI_FALSE);
        failed();
    }
    void set_obj(jobject obj, jfieldID id, jobject v) const {
        if (!obj || !id) return;
        env->SetObjectField(obj, id, v);
        failed();
    }

    // ---- odds and ends ---------------------------------------------------

    bool same_object(jobject a, jobject b) const {
        return env->IsSameObject(a, b) == JNI_TRUE;
    }
    bool is_instance(jobject obj, jclass cls) const {
        if (!obj || !cls) return false;
        return env->IsInstanceOf(obj, cls) == JNI_TRUE;
    }
    jobject global(jobject obj) const {
        if (!obj) return nullptr;
        return env->NewGlobalRef(obj);
    }
    void delete_local(jobject obj) const {
        if (obj) env->DeleteLocalRef(obj);
    }

    /// A Java string as UTF-8.
    std::string to_string(jobject s) const {
        if (!s) return {};
        const char* chars = env->GetStringUTFChars(static_cast<jstring>(s), nullptr);
        if (!chars || failed()) return {};
        std::string out(chars);
        env->ReleaseStringUTFChars(static_cast<jstring>(s), chars);
        return out;
    }
};

/// A local reference frame: everything made inside it is released together.
/// Without this the per-frame scanning leaks references until the JVM complains.
class Frame {
  public:
    Frame(const Env& e, jint capacity) : env_(e.env) {
        ok_ = env_ && env_->PushLocalFrame(capacity) == 0;
    }
    ~Frame() {
        if (ok_) env_->PopLocalFrame(nullptr);
    }
    Frame(const Frame&) = delete;
    Frame& operator=(const Frame&) = delete;
    explicit operator bool() const { return ok_; }

  private:
    JNIEnv* env_ = nullptr;
    bool ok_ = false;
};

}  // namespace lodestone::jni