Implement hookClassInitializer API for static initializers (#535)

Since standard Java Reflection cannot access the `<clinit>` method (class's static initializer), we add a new native function, `HookBridge.getStaticInitializer`, which uses JNI `GetStaticMethodID` to retrieve a `Method` handle for it. The `LSPosedContext` API then passes this handle to the existing `doHook` machinery.

There are two possible exceptions thrown by the API:

- `IllegalArgumentException`: Thrown for user-level errors, such as attempting to hook a class that has no static initializer block (`static { ... }`). This is a predictable failure based on the provided class.

- `HookFailedError`: Thrown when the underlying native hooking engine (`lsplant`) fails to install the hook. This indicates a framework or environment-level issue (e.g., ART incompatibility, method inlining by the JIT/AOT compiler) and is not a fault in the module's logic.
This commit is contained in:
JingMatrix 2026-02-13 19:35:14 +01:00
parent cdc536f10b
commit fc718e739b
4 changed files with 47 additions and 1 deletions

View File

@ -189,6 +189,27 @@ public class LSPosedContext implements XposedInterface {
return LSPosedBridge.doHook(origin, priority, hooker); return LSPosedBridge.doHook(origin, priority, hooker);
} }
@Override
@NonNull
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, @NonNull Class<? extends Hooker> hooker) {
return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
@SuppressWarnings({"unchecked", "rawtypes"})
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, int priority, @NonNull Class<? extends Hooker> hooker) {
Method staticInitializer = HookBridge.getStaticInitializer(origin);
// The class might not have a static initializer block
if (staticInitializer == null) {
throw new IllegalArgumentException("Class " + origin.getName() + " has no static initializer");
}
// Use the existing doHook logic. It will return a MethodUnhooker<Method>.
return (MethodUnhooker) LSPosedBridge.doHook(staticInitializer, priority, hooker);
}
private static boolean doDeoptimize(@NonNull Executable method) { private static boolean doDeoptimize(@NonNull Executable method) {
if (Modifier.isAbstract(method.getModifiers())) { if (Modifier.isAbstract(method.getModifiers())) {
throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method); throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method);

View File

@ -1,6 +1,7 @@
package org.lsposed.lspd.nativebridge; package org.lsposed.lspd.nativebridge;
import java.lang.reflect.Executable; import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import dalvik.annotation.optimization.FastNative; import dalvik.annotation.optimization.FastNative;
@ -25,4 +26,12 @@ public class HookBridge {
public static native boolean setTrusted(Object cookie); public static native boolean setTrusted(Object cookie);
public static native Object[][] callbackSnapshot(Class<?> hooker_callback, Executable method); public static native Object[][] callbackSnapshot(Class<?> hooker_callback, Executable method);
/**
* Retrieves the static initializer (<clinit>) of a class as a Method object.
* Standard Java reflection cannot access this.
* @param clazz The class to inspect.
* @return A Method object for the static initializer, or null if it doesn't exist.
*/
public static native Method getStaticInitializer(Class<?> clazz);
} }

View File

@ -322,6 +322,21 @@ LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callbac
return res; return res;
} }
LSP_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) {
// <clinit> is the internal name for a static initializer.
// Its signature is always ()V (no arguments, void return).
jmethodID mid = env->GetStaticMethodID(target_class, "<clinit>", "()V");
if (!mid) {
// If GetStaticMethodID fails, it throws an exception.
// We clear it and return null to let the Java side handle it gracefully.
env->ExceptionClear();
return nullptr;
}
// Convert the method ID to a java.lang.reflect.Method object.
// The last parameter must be JNI_TRUE because it's a static method.
return env->ToReflectedMethod(target_class, mid, JNI_TRUE);
}
static JNINativeMethod gMethods[] = { static JNINativeMethod gMethods[] = {
LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"), LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"), LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"),
@ -332,6 +347,7 @@ static JNINativeMethod gMethods[] = {
LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"),
LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"), LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, getStaticInitializer, "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"),
}; };
void RegisterHookBridge(JNIEnv *env) { void RegisterHookBridge(JNIEnv *env) {

@ -1 +1 @@
Subproject commit 64e29bd657ef4d2540b34402f5a988778f29e676 Subproject commit b896dbcda3fa1550d04d43d962923318ed5a61a8