Skip to main content

Interacting with Native Structs

Manually reading and writing memory with offsets is powerful, but it quickly becomes unmanageable for complex data structures. If you get an offset wrong, your app will likely crash.

Terminator solves this with a beautiful and type-safe Struct mapping system. It allows you to define a Kotlin class that directly mirrors a native C/C++ struct. The framework will then handle all offset calculations and memory access for you automatically.

Defining a Struct Wrapper

Let's model a C++ struct in Kotlin.

The C/C++ Struct:

// Generated cpp
struct Vector3 {
float x;
float y;
float z;
};

struct PlayerProfile {
int64_t playerId; // A 64-bit integer
int32_t level; // A 32-bit integer
Vector3 lastPosition; // A nested struct
char name[32]; // A fixed-size C-string (char array)
};

The Kotlin Mapping:

// Generated kotlin
import com.skiy.terminatorkt.Pointer
import com.skiy.terminatorkt.memory.Struct
import com.skiy.terminatorkt.memory.StructCompanion

// 1. Define the nested struct first.
class Vector3(pointer: Pointer) : Struct(pointer) {
// 2. The companion object defines the layout.
companion object : StructCompanion<Vector3>() {
// 3. Declare fields in the EXACT order they appear in the C struct.
val x = float("x")
val y = float("y")
val z = float("z")
// 4. Implement the required internal factory.
override fun create(pointer: Pointer) = Vector3(pointer)
}

// 5. Create properties that delegate to the framework.
var x: Float by Mapped()
var y: Float by Mapped()
var z: Float by Mapped()
}

// Now, define the main struct.
class PlayerProfile(pointer: Pointer) : Struct(pointer) {
companion object : StructCompanion<PlayerProfile>() {
val playerId = long("playerId")
val level = int("level")
val lastPosition = nested("lastPosition", Vector3) // nested struct
val name = string("name", 32) // fixed-size C-string

override fun create(pointer: Pointer) = PlayerProfile(pointer)
}

var playerId: Long by Mapped()
var level: Int by Mapped()
var lastPosition: Vector3 by Mapped()
var name: String by Mapped()
}

Anatomy of a Struct Wrapper

  • Inherit from Struct: Your class must extend Struct(pointer).
  • companion object: Defines the memory layout. Must extend StructCompanion<YourClass>.
  • Field Declaration: Use helper methods like int(), long(), float(), nested(), and string().
  • create Factory: Required factory method: create(pointer).
  • Property Delegation: Define var properties using by Mapped().

Using the Struct Wrapper

Once defined, using the struct is incredibly intuitive.

Wrapping an Existing Pointer

// Inside a hook callback...
onCalled {
// Assume the first argument is a pointer to a PlayerProfile.
val profilePtr = arg<Pointer>(0)

// Use the .of() factory method to safely wrap the raw pointer.
val profile = PlayerProfile.of(profilePtr)

// Access fields like a Kotlin data class
println("Hooked player: ${profile.name} (Level ${profile.level})")

// Access nested properties
val xPos = profile.lastPosition.x
println("Player X position: $xPos")

// Modify properties directly
profile.level += 1
profile.lastPosition.x = 0f
}

Creating a New Struct

import com.skiy.terminatorkt.withTempArena

fun createAndPassNewProfile() {
withTempArena { arena ->
val newProfile = PlayerProfile.create(arena)

newProfile.playerId = 1337L
newProfile.level = 1
newProfile.name = "Terminator"
newProfile.lastPosition.x = 100f
newProfile.lastPosition.y = 200f
newProfile.lastPosition.z = 300f

// Pass pointer to native function
// someNativeFunction(newProfile.pointer)
} // Memory auto-freed here
}

The struct mapping system is a cornerstone of Terminator, transforming complex and dangerous memory operations into a safe, readable, and highly productive experience.

What's Next?

You now know how to hook native functions and handle complex data. Let's switch gears and explore the other side of the framework: hooking JVM methods.

➡️ Next: Callable Pointers & Out-Parameters