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.
- Kotlin
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.
- Kotlin
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.
- Kotlin
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