Skip to main content

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:

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

  1. Type Inference: Uses a reified generic (<T>) to inspect the Kotlin function type.
  2. Descriptor Generation: Automatically generates the required FunctionDescriptor.
  3. Argument Marshalling: Converts Kotlin types into native representations.
  4. Return Value Marshalling: Converts the native return value into Kotlin.
  5. 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:

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

  1. Declaration: Create an Out<T> object, specifying the type.
  2. Allocation: Terminator allocates native memory and passes a pointer.
  3. Retrieval: Native writes the result to memory.
  4. 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