Skip to main content

Working with Pointers and Memory

A huge part of native programming involves direct memory manipulation. To effectively hook native functions, you need to be able to read and write to the same memory addresses that the C/C++ code uses. Terminator provides a powerful and safe API for this, abstracting away the complexities of the underlying FFM API.


The Pointer Typealias

The core of the memory API is the Pointer. In Terminator's Kotlin API, Pointer is a typealias for the MemorySegment class from the underlying Panama Port. Think of it as a type-safe object that holds a native memory address.

You'll encounter pointers everywhere:

  • As arguments to hooked functions (const char*, int*, etc.)
  • As return values from functions
  • As fields within native structs

Pointer Basics

Here are the most common operations you'll perform with pointers.

import com.skiy.terminatorkt.*

fun pointerBasics() {
// A function returns a pointer (can be from a hook, a struct, etc.)
val basePointer: Pointer = getSomePointerFromSomewhere()

// 1. Check for NULL
if (basePointer.isNull) {
println("Pointer is NULL!")
return
}

// You can also throw if it's null
basePointer.checkNull("Base Pointer")

// 2. Get the raw address as a Long
val address: Long = basePointer.address
println("Pointer address: 0x${address.toString(16)}")

// 3. Convert a Long back to a Pointer
val samePointer = address.toPointer()

// 4. Pointer Arithmetic (creates a new pointer at an offset)
val pointerPlus8Bytes = basePointer + 8 // Move 8 bytes forward
val pointerPlus16Bytes = basePointer + 0x10 // Hex offsets are common

println("New pointer address: 0x${pointerPlus8Bytes.address.toString(16)}")
}

Reading and Writing Primitive Types

Once you have a valid pointer, you can read or write data at its memory location using a set of convenient extension functions.

import com.skiy.terminatorkt.*

// Assume playerPtr is a Pointer to a C++ Player object
fun readPlayerData(playerPtr: Pointer) {
// C++ struct might look like this:
// struct Player {
// int32_t id; // at offset 0
// int32_t score; // at offset 4
// float health; // at offset 8
// Player* nemesis; // at offset 12 (on 32-bit) or 16 (on 64-bit)
// };

// Read values using offsets from the base pointer
val id: Int = playerPtr.readInt(0)
val score: Int = playerPtr.readInt(4)
val health: Float = playerPtr.readFloat(8)

// Read another pointer
val nemesisPtr: Pointer = playerPtr.readPointer(16) // Assuming 64-bit platform

println("Player ID: $id, Score: $score, Health: $health")
if (!nemesisPtr.isNull) {
println("Nemesis pointer: 0x${nemesisPtr.address.toString(16)}")
}

// You can also WRITE data back to memory
// Let's give the player a score boost
playerPtr.writeInt(4, 9999)
}

Terminator provides read/write functions for all primitive types: Byte, Short, Int, Long, Float, Double, and even Pointer itself.


Working with Strings (CString)

C-style strings (const char*) are just pointers to a sequence of bytes ending with a null character (\0). Terminator's memory API makes working with them trivial.

import com.skiy.terminatorkt.*

fun stringManipulation() {
// 1. Reading a C-string from a pointer
val messagePtr: Pointer = getMessagePointer() // e.g., from a hook argument
if (!messagePtr.isNull) {
// readCString automatically finds the null terminator
val message: String = messagePtr.readCString()
println("Read message: $message")
}

// 2. Writing a Kotlin String to native memory
// This requires an Arena to manage the memory allocation.
withTempArena { arena ->
val myString = "Hello from Kotlin!"

// toCString allocates memory in the arena and copies the string into it
val myStringPtr: Pointer = myString.toCString(arena)

// Now you can pass myStringPtr to a native function
// e.g., callOriginal(myStringPtr)
println("Created native string at 0x${myStringPtr.address.toString(16)}")
} // Memory allocated for the string is automatically freed here
}

What is an Arena?

An Arena is a memory manager. When you allocate memory within an arena (like for a C-string), the arena keeps track of it. When the arena is closed, all memory it manages is freed at once. The withTempArena { ... } block is a convenient helper that creates a temporary arena and automatically closes it for you, preventing memory leaks.


What's Next?

Manually calculating offsets for struct fields is tedious and error-prone. Terminator provides a far superior mechanism for this.

➡️ Next: Interacting with Native Structs