
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=noneEzHook works similarly to Lancet, but for Kotlin Multiplatform IR.
A hook is simply:
@EzHook, @EzHook.Before, @EzHook.After, or @EzHook.NULL
A hook function must:
callOrigin()
@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 hook
isInitializeProperty
init {} blocks run only if you call the original constructorval 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() = nullinit {} blocks never runisInitializeProperty:
val old = getField<String>()setField(value + " modified")Delegated property case:
getField() returns the delegate instance, not its internal value.val 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:
@HiddenFromObjC
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=noneEzHook works similarly to Lancet, but for Kotlin Multiplatform IR.
A hook is simply:
@EzHook, @EzHook.Before, @EzHook.After, or @EzHook.NULL
A hook function must:
callOrigin()
@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 hook
isInitializeProperty
init {} blocks run only if you call the original constructorval 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() = nullinit {} blocks never runisInitializeProperty:
val old = getField<String>()setField(value + " modified")Delegated property case:
getField() returns the delegate instance, not its internal value.val 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:
@HiddenFromObjC