Odin Programming Language: Explicit, Performant, dan Joyful Systems Programming
Odin adalah bahasa pemrograman tingkat rendah yang menekankan kesederhanaan, performa, dan "joy of programming". Dengan first-class SIMD support, manual memory management, dan filosofi "explicit over implicit", Odin sangat cocok untuk game development dan sistem programming.
📋 Daftar Isi
1. Mengapa Odin?
Odin dibuat oleh Ginger Bill (William Schwartz) sebagai alternatif C yang lebih modern dan menyenangkan. Odin digunakan oleh beberapa studio game besar, termasuk developer Polylane dan beberapa proyek game indie.
- Explicit over Implicit — Tidak ada hidden behavior
- Manual Memory — Full control dengan allocator interface
- First-class SIMD — Built-in SIMD vector types
- No Hidden Allocations — Anda tahu persis di mana memori dialokasikan
- C Interop — Kompabilitas langsung dengan C libraries
- Game Ready — Vendor libraries untuk Vulkan, OpenGL, SDL, dll
- Simple Sintaks — Hanya ~25 keywords
Odin memiliki vendor packages yang sudah include untuk Vulkan, OpenGL, SDL2, DirectX, XInput, dan banyak lagi. Ini menjadikan Odin sangat siap untuk game development dari hari pertama.
2. Instalasi
# Download dari odin-lang.org # https://odin-lang.org/docs/install/ # Linux/macOS git clone https://github.com/odin-lang/Odin.git cd Odin make release # macOS (Homebrew) brew install odin # Windows # Download pre-built binary dari GitHub releases # Atau build dari source dengan Visual Studio # Verifikasi odin version # odin version dev-2024-xx:xx:xxx # Jalankan file odin run hello.odin # Build odin build hello.odin # Build dengan optimasi odin build hello.odin -o:speed odin build hello.odin -o:size odin build hello.odin -o:none # debug # Check (type checking tanpa compile) odin check hello.odin # Vet (static analysis) odin vet hello.odin # Buat proyek baru mkdir my_project && cd my_project mkdir src # Buat src/main.odin odin run src
3. Dasar-Dasar Odin
Variables dan Types
package main
import "core:fmt"
import "core:strings"
main :: proc() {
// Variables
nama := "BeebaneLabs" // inferred type: string
umur := 25 // inferred type: int
tinggi := 1.75 // inferred type: f64
aktif := true // inferred type: bool
// Explicit types
x: int = 42
y: f64 = 3.14
s: string = "hello"
b: bool = true
c: byte = 'a'
n: u8 = 255
// Type sizes: i8, i16, i32, i64, int
// u8, u16, u32, u64, uint
// f32, f64
// rune (Unicode codepoint)
// Constants
PI :: 3.14159
MAX_SIZE :: 100
GREETING :: "Hello"
// Typed constants
MAX_INT :: int(2147483647)
// Mutable (default immutable)
mut skor := 100
skor += 50
// Strings
pesan := fmt.tprintf("Nama: %s, Umur: %d", nama, umur)
fmt.println(pesan)
fmt.println(strings.to_upper("hello"))
fmt.println(strings.to_lower("WORLD"))
fmt.println(strings.contains("hello world", "world"))
fmt.println(strings.replace("hello world", "world", "Odin", 1))
// Arrays (fixed size)
angka := [5]int{1, 2, 3, 4, 5}
fmt.println(angka[0])
// Dynamic arrays (slices)
mut list := make([dynamic]int)
defer delete(list)
append(&list, 1)
append(&list, 2)
append(&list, 3)
fmt.println(list)
// Maps
mut user := make(map[string]string)
defer delete(user)
user["nama"] = "Andi"
user["email"] = "andi@email.com"
fmt.println(user["nama"])
// Structs
Point :: struct {
x, y: f64,
}
p := Point{3.0, 4.0}
fmt.println(p.x, p.y)
// Enums
Color :: enum {
Red,
Green,
Blue,
}
c := Color.Red
// Bit sets
Permissions :: bit_set[Permission]
Permission :: enum {
Read,
Write,
Execute,
}
mut perms := Permissions{.Read, .Write}
// Control flow
if umur >= 17 {
fmt.println("Dewasa")
} else {
fmt.println("Anak-anak")
}
// Switch
switch c {
case .Red:
fmt.println("Merah")
case .Green:
fmt.println("Hijau")
case .Blue:
fmt.println("Biru")
}
// For loops (hanya satu bentuk: for)
for i in 0..<10 {
fmt.println(i)
}
for item in angka {
fmt.println(item)
}
mut i := 0
for i < 10 {
fmt.println(i)
i += 1
}
// For dengan multiple values
for val, idx in angka {
fmt.printf("[%d] = %d\n", idx, val)
}
}
Procedures dan Methods
package main
import "core:fmt"
import "core:math"
// Basic procedure
tambah :: proc(a, b: int) -> int {
return a + b
}
// Named return value
min_max :: proc(arr: []int) -> (min, max: int) {
min = arr[0]
max = arr[0]
for val in arr {
if val < min { min = val }
if val > max { max = val }
}
return
}
// Multiple return values
divide :: proc(a, b: f64) -> (result: f64, ok: bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
// Default parameters (via overloading)
greet :: proc(name: string) -> string {
return greet(name, "Halo")
}
greet :: proc(name, greeting: string) -> string {
return greeting + ", " + name + "!"
}
// Polymorphic procedures (generics)
first :: proc(arr: $T/$[]$E) -> E {
return arr[0]
}
contains :: proc(arr: $T/[]$E, val: E) -> bool {
for item in arr {
if item == val {
return true
}
}
return false
}
// Struct dengan methods
Vector2 :: struct {
x, y: f64,
}
length :: proc(v: Vector2) -> f64 {
return math.sqrt(v.x*v.x + v.y*v.y)
}
add :: proc(a, b: Vector2) -> Vector2 {
return Vector2{a.x + b.x, a.y + b.y}
}
dot :: proc(a, b: Vector2) -> f64 {
return a.x*b.x + a.y*b.y
}
// Procedures sebagai parameter
apply :: proc(arr: []int, f: proc(int) -> int) {
for &val in arr {
val = f(val)
}
}
// Procedure groups (overloading)
add :: proc(a, b: int) -> int { return a + b }
add :: proc(a, b: f64) -> f64 { return a + b }
add :: proc(a, b: string) -> string { return a + b }
// Using context
custom_allocator :: proc() {
buf: [1024]byte
arena := make(allocator.Arena, buf[:])
context.allocator = arena.allocator()
// All allocations here use the arena
data := make([]int, 100)
_ = data
}
4. Manual Memory Management
Odin memiliki pendekatan yang sangat baik untuk memory management: explicit allocators yang di-pass melalui context.
package main
import "core:fmt"
import "core:mem"
import "core:os"
// Default allocator (heap)
default_example :: proc() {
data := make([]int, 100)
defer delete(data)
fmt.println(len(data))
}
// Arena allocator
arena_example :: proc() {
backing: [1024 * 1024]byte // 1MB di stack
arena := make(mem.Arena, backing[:])
defer destroy(&arena)
// Semua alokasi di sini pakai arena
old_alloc := context.allocator
context.allocator = arena.allocator()
defer { context.allocator = old_alloc }
data := make([]int, 1000) // dialokasikan di arena
// Tidak perlu delete! Arena akan di-destroy sekaligus
fmt.println(len(data))
}
// Temp allocator (very fast, scratch memory)
temp_example :: proc() {
buf := make([]byte, 0, 1024, context.temp_allocator)
append(&buf, "Hello"...)
fmt.println(string(buf))
}
// Tracking allocator (debug)
tracking_example :: proc() {
tracker: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracker, context.allocator)
defer mem.tracking_allocator_destroy(&tracker)
context.allocator = mem.tracking_allocator(&tracker)
data := make([]int, 100)
defer delete(data)
// Print allocation stats
stats := mem.tracking_allocator_get_stats(&tracker)
fmt.printf("Allocations: %d\n", stats.allocation_count)
fmt.printf("Bytes allocated: %d\n", stats.allocated_bytes)
}
// Custom allocator interface
Logging_Allocator :: struct {
parent: mem.Allocator,
}
logging_alloc :: proc(
self: ^Logging_Allocator,
mode: mem.Allocator_Mode,
size: int,
old_size: int,
old_alignment: int,
) -> ([]byte, mem.Allocator_Error) {
fmt.printf("Alloc: mode=%v, size=%d\n", mode, size)
return mem.alloc(self.parent, mode, size, old_size, old_alignment)
}
// Context system - semua allocator di-pass melalui context
main :: proc() {
// Default context
default_example()
// Custom allocator
allocator := mem.JS_Pages // atau custom
context.allocator = allocator
}
5. SIMD (Single Instruction Multiple Data)
Odin memiliki first-class SIMD support, memungkinkan operasi paralel pada data yang sangat cepat.
package main
import "core:fmt"
import "core:math"
import "core:intrinsics"
// SIMD vector types (built-in)
// f32x4, f32x8, f64x2, f64x4
// i32x4, i32x8, i16x8, i16x16
simd_vector_add :: proc() {
a := f32x4{1.0, 2.0, 3.0, 4.0}
b := f32x4{5.0, 6.0, 7.0, 8.0}
c := a + b // {6.0, 8.0, 10.0, 12.0}
fmt.println(c)
}
simd_vector_multiply :: proc() {
a := f32x4{1.0, 2.0, 3.0, 4.0}
b := f32x4{2.0, 2.0, 2.0, 2.0}
c := a * b // {2.0, 4.0, 6.0, 8.0}
fmt.println(c)
}
// SIMD dot product
simd_dot :: proc(a, b: f32x4) -> f32 {
c := a * b
return c[0] + c[1] + c[2] + c[3]
}
// SIMD di game math
simd_matrix_multiply :: proc() {
// Vector4 operations
position := f32x4{1.0, 2.0, 3.0, 1.0}
scale := f32x4{2.0, 2.0, 2.0, 1.0}
scaled := position * scale
fmt.println(scaled) // {2.0, 4.0, 6.0, 1.0}
}
// Batch processing dengan SIMD
simd_process_array :: proc(input: []f32, output: []f32, scalar: f32) {
assert(len(input) == len(output))
assert(len(input) % 4 == 0)
s := f32x4{scalar, scalar, scalar, scalar}
for i in 0.. f32 {
// SSE3 hadd
tmp := intrinsics.simd_shuffle(v, v, 1, 0, 3, 2) // [1,0,3,2]
sum1 := v + tmp // [0+1, 1+0, 2+3, 3+2]
tmp2 := intrinsics.simd_shuffle(sum1, sum1, 2, 3, 0, 1)
sum2 := sum1 + tmp2 // semua elemen = total
return sum2[0]
}
// SIMD comparison
simd_filter :: proc() {
a := f32x4{1.0, 5.0, 3.0, 8.0}
threshold := f32x4{4.0, 4.0, 4.0, 4.0}
mask := a > threshold // {false, true, false, true}
fmt.println(mask)
}
6. C Interop
package main
import "core:fmt"
// Import C library
foreign import lib "system:c"
// C function declarations
foreign lib {
// Basic C functions
C.sqrt :: proc(x: f64) -> f64 ---
C.pow :: proc(base: f64, exp: f64) -> f64 ---
C.sin :: proc(x: f64) -> f64 ---
C.cos :: proc(x: f64) -> f64 ---
C.atan2 :: proc(y: f64, x: f64) -> f64 ---
// String functions
C.strlen :: proc(s: cstring) -> uint ---
C.strcmp :: proc(a, b: cstring) -> int ---
// Memory
C.malloc :: proc(size: int) -> rawptr ---
C.free :: proc(ptr: rawptr) ---
C.memset :: proc(dest: rawptr, c: int, n: int) -> rawptr ---
C.memcpy :: proc(dest, src: rawptr, n: int) -> rawptr ---
}
// Link dengan custom C library
// foreign import my_lib "custom:mylib.a"
// foreign my_lib {
// C.my_function :: proc(x: int) -> int ---
// }
// Struct yang compatible dengan C
C_Compatible :: struct #align(1) {
x: f32,
y: f32,
z: f32,
}
main :: proc() {
// Panggil C functions
fmt.println(C.sqrt(144.0)) // 12.0
fmt.println(C.pow(2.0, 10.0)) // 1024.0
fmt.println(C.sin(3.14159 / 2.0)) // ~1.0
// String interop
cstr := cstring("Hello from C!")
fmt.println(C.strlen(cstr)) // 13
// Memory
buf := C.malloc(1024)
C.memset(buf, 0, 1024)
C.free(buf)
}
// Using vendor C libraries
// import "vendor:SDL2"
// import "vendor:vulkan"
// import "vendor:raylib"
7. Explicit Over Implicit
Filosofi core Odin: semua harus eksplisit, tidak ada hidden behavior yang mengejutkan programmer.
package main
import "core:fmt"
import "core:strings"
// 1. Tidak ada hidden allocation
// Setiap make(), new(), atau slice allocation harus eksplisit
explicit_allocation :: proc(allocator: mem.Allocator) {
// Anda TAHU ini mengalokasikan memori
data := make([]int, 100, allocator)
defer delete(data, allocator)
}
// 2. Tidak ada hidden control flow
// - Tidak ada exception
// - Tidak ada implicit return
// - Tidak ada operator overloading
// - Tidak ada implicit conversion
// 3. Explicit error handling
read_file :: proc(path: string) -> (content: string, ok: bool) {
data, success := os.read_entire_file(path)
if !success {
return "", false
}
return string(data), true
}
// 4. Explicit type conversion
explicit_types :: proc() {
x: int = 42
y: f64 = f64(x) // explicit cast
z: int = int(y) // explicit cast
s: string = fmt.tprintf("%d", x) // explicit conversion
_ = y; _ = z; _ = s
}
// 5. Explicit bit manipulation
bit_ops :: proc() {
flags: u8 = 0b1010
flags |= 0b0001 // set bit
flags &= ~0b1000 // clear bit
has_bit := flags & 0b0001 != 0 // check bit
_ = has_bit
}
// 6. No implicit copies
// Arrays dan structs harus di-copy secara eksplisit
copy_example :: proc() {
a := [3]int{1, 2, 3}
b := a // explicit copy (value semantics)
b[0] = 99
fmt.println(a[0]) // still 1
fmt.println(b[0]) // 99
}
// 7. Explicit distinction value/reference
// Value types (structs, arrays, enums)
// Reference types (maps, dynamic arrays, pointers)
// 8. No hidden defer/RAII
// defer adalah eksplisit
defer_example :: proc() -> int {
resource := acquire_resource()
defer release_resource(resource)
return process(resource)
}
8. Game Development dengan Odin
package game
import "core:fmt"
import "vendor:raylib"
// Game state
Game :: struct {
player_pos: raylib.Vector2,
player_speed: f32,
screen_width: i32,
screen_height: i32,
}
init_game :: proc() -> Game {
return Game{
player_pos = raylib.Vector2{400, 300},
player_speed = 300.0,
screen_width = 800,
screen_height = 600,
}
}
update :: proc(game: ^Game, dt: f32) {
// Input handling
if raylib.IsKeyDown(.W) || raylib.IsKeyDown(.UP) {
game.player_pos.y -= game.player_speed * dt
}
if raylib.IsKeyDown(.S) || raylib.IsKeyDown(.DOWN) {
game.player_pos.y += game.player_speed * dt
}
if raylib.IsKeyDown(.A) || raylib.IsKeyDown(.LEFT) {
game.player_pos.x -= game.player_speed * dt
}
if raylib.IsKeyDown(.D) || raylib.IsKeyDown(.RIGHT) {
game.player_pos.x += game.player_speed * dt
}
}
draw :: proc(game: Game) {
raylib.BeginDrawing()
defer raylib.EndDrawing()
raylib.ClearBackground(raylib.RAYWHITE)
raylib.DrawCircle(i32(game.player_pos.x), i32(game.player_pos.y), 20, raylib.BLUE)
raylib.DrawText("WASD to move", 10, 10, 20, raylib.DARKGRAY)
raylib.DrawFPS(10, 40)
}
main :: proc() {
game := init_game()
raylib.InitWindow(game.screen_width, game.screen_height, "Odin Game")
defer raylib.CloseWindow()
raylib.SetTargetFPS(60)
for !raylib.WindowShouldClose() {
dt := raylib.GetFrameTime()
update(&game, dt)
draw(game)
}
}
// Build:
// odin run src/game -o:speed