Callable Pointers & Out-Parameters
Beyond hooking, a major part of native interaction involves calling native functions from your own code. Terminator makes this incredibly simple with CallablePointer
. We will also cover how to handle a common C API pattern: "out-parameters," where a function returns a value through a pointer argument.
asCallable()
: Turning Pointers into Functions
Imagine you have a Pointer
to a native function. It could be from a symbol lookup, a struct field, or a hook argument. How do you call it? The asCallable()
extension function transforms any Pointer
into a type-safe, invokable Kotlin function.
The C/C++ Functions:
// A simple math function
int32_t multiply(int32_t a, int32_t b);
// A function that creates and returns a pointer to a PlayerProfile struct
PlayerProfile* create_default_profile(const char* name);
Calling them from Kotlin:
- Kotlin
import com.skiy.terminatorkt.*
import com.v7878.foreign.SymbolLookup
fun callNativeFunctions() {
val lookup = SymbolLookup.loaderLookup()
// --- Example 1: Simple math function ---
val multiplyPtr = lookup.find("multiply").get()
val multiply = multiplyPtr.asCallable<(Int, Int) -> Int>()
val result: Int = multiply(10, 5) as Int
println("Native multiply(10, 5) = $result") // Prints 50
// --- Example 2: Function returning a struct pointer ---
val createProfilePtr = lookup.find("create_default_profile").get()
val createProfile = createProfilePtr.asCallable<(CString) -> PlayerProfile>()
withTempArena { arena ->
val profile: PlayerProfile = createProfile("Nazar") as PlayerProfile
println("Created profile for ${profile.name} with level ${profile.level}")
}
}
How asCallable
Works
- Type Inference: Uses a
reified
generic (<T>
) to inspect the Kotlin function type. - Descriptor Generation: Automatically generates the required
FunctionDescriptor
. - Argument Marshalling: Converts Kotlin types into native representations.
- Return Value Marshalling: Converts the native return value into Kotlin.
- Caching: Result is cached for subsequent calls.
Handling Out-Parameters with Out<T>
Many C APIs "return" values by writing to pointers passed as arguments.
C/C++ Function:
// Gets the screen dimensions, writing them to the provided pointers.
void get_screen_size(int32_t* out_width, int32_t* out_height);
Kotlin Example:
- Kotlin
import com.skiy.terminatorkt.*
fun getScreenDimensions() {
val getScreenSizePtr = getFunctionPointer("get_screen_size")
val getScreenSize = getScreenSizePtr.asCallable<(Pointer, Pointer) -> Unit>()
val widthOut = out<Int>()
val heightOut = out<Int>()
getScreenSize(widthOut, heightOut)
val width = widthOut.value
val height = heightOut.value
println("Screen dimensions: ${width}x${height}")
}
How Out<T>
Works
- Declaration: Create an
Out<T>
object, specifying the type. - Allocation: Terminator allocates native memory and passes a pointer.
- Retrieval: Native writes the result to memory.
- Access:
.value
reads the result back into Kotlin.
This pattern turns a complex, unsafe memory operation into a simple, declarative process.
✅ What's Next?
You have now mastered native function hooking, memory manipulation, and function calling. It's time to switch gears and explore the other side of the framework: hooking JVM methods.
➡️ Next: Your First JVM Hook