EzHook
0.0.4indexedCompile-time AOP that replaces functions, constructors and properties with zero runtime reflection and no performance cost; supports before/after/NULL hooks, callOrigin/getField/getThisRef and inline hooks.
Compile-time AOP that replaces functions, constructors and properties with zero runtime reflection and no performance cost; supports before/after/NULL hooks, callOrigin/getField/getThisRef and inline hooks.
EzHook is an AOP (Aspect‑Oriented Programming) framework for Kotlin Multiplatform, supporting Kotlin/Native and Kotlin/JS.
It replaces functions, constructors, and properties at compile time, with zero runtime reflection and no performance cost.
EzHook consists of two components:
buildscript {
dependencies {
classpath("io.github.dreammooncai:ez-hook-gradle-plugin:0.0.4")
}
}
plugins {
id("io.github.dreammooncai.ez-hook-gradle-plugin")
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.dreammooncai:ez-hook-library:0.0.4")
}
}
}
kotlin.native.cacheKind=none
EzHook works similarly to Lancet, but for Kotlin Multiplatform IR.
A hook is simply:
@EzHook, @EzHook.Before, @EzHook.After, or @EzHook.NULLA hook function must:
@HiddenFromObjC
@EzHook("kotlin.time.Duration.toInt")
fun toInt(unit: DurationUnit): Int {
println("Hook to int")
return 10086
}
@EzHook("kotlin.time.Duration.toInt")
fun toInt(unit: DurationUnit): Int {
val unit = DurationUnit.HOURS
return callOrigin<Int>() // now uses HOURS
}
callOrigin() always uses the current overridden parameters.
return callOrigin<Int>(null, 123, "xyz")
null must be explicitEzHook supports primary and secondary constructors.
@EzHook("com.example.MyClass.<init>")
fun hookConstructor(name: String) {
val name = "Modified"
callOrigin<Unit>()
}
this → use getThisRef<T>()isInitializeProperty controls whether Kotlin property initializers run before the hookthisval self = getThisRef<MyClass>()
Works for:
Not available for top‑level functions.
EzHook can hook:
@EzHook("com.example.MyClass.prop")
var newProp = "777777"
get() = callOrigin<String>() + "3333"
set(value) { setField(value + "22222") }
getField() / setField() access the backing field@EzHook.Before("com.example.MyClass.prop.get")
fun beforeGet() {
println("before getter")
}
@EzHook.After("com.example.MyClass.prop.set")
fun afterSet(value: String) {
println("setter finished with $value")
}
Runs before the target method/constructor/property.
@EzHook.Before("com.example.MyClass.test")
fun beforeTest(name: String) {
println("before test")
}
Runs after the target.
If it returns a non‑Unit value → overrides the target’s return value.
@EzHook.After("com.example.MyClass.test")
fun afterTest(name: String): String {
return "hooked result"
}
NULL hooks replace the target and force it to return null.
@EzHook.NULL("com.example.MyClass.loadData")
fun forceNull() = null
init {} blocks never runisInitializeProperty:
val old = getField<String>()
setField(value + " modified")
Delegated property case:
getField() returns the delegate instance, not its internal value.this Propertiesval username = getThisProperty<String>("username")
setThisProperty("count", 5)
If isBackingField = true → operate on the backing field instead of the getter/setter.
Full support.
@EzHook("com.example.topLevelFunction")
fun topLevelFunction(name: String): String {
val name = "override"
return "origin: ${callOrigin<String>()}, new: $name"
}
Top‑level property:
@EzHook("com.example.topLevelProp")
var topLevelProp = "666"
get() = getField<String>() + "444"
set(value) { setField(value + "555") }
@EzHook("com.example.getStr")
fun Int.getStr(): String {
return callOrigin<String>() + "-new2"
}
Inline mode eliminates cross‑module linking:
@EzHook("kotlin.time.Duration.toInt", inline = true)
fun toInt(unit: DurationUnit): Int {
return callOrigin<Int>()
}
Use inline = true when:
@HiddenFromObjCcallOrigin()isInitializePropertyinit {} blocks run only if you call the original constructorSurfaced from shared tags and platforms — no rankings paid for.