Instuction Yapısı

Bu bölümde Rust'ta enum cinsinden yeni bir yapı tanımlama şeklini göreceğiz. Rust'ta enum'lar farklı biçimlerde yapılar içerebilir. Örneğin bir enum içerisinde yer alan yapı: unit benzeri hiç bir eleman içermeyen, tuple cinsinden ya da C benzeri bir struct olabilir. Her bir geçerli struct yapısı, aynı zamanda geçerli bir enum biçimidir.

Rust'ın yine en güçlü özelliklerinden biri olan pattern matching, enum yapısını kolay bir şekilde parçalayarak işlemeye yarar. Yazacağımız emülatörün okunabilir olabilmesi için, 16-bitlik sayı olarak okuduğumuz Opcode'u, enum ile okunabilir bir yapıya dönüştüreceğiz.

Bu işlemi yapmadan önce 16-bit uzunluğunda olan ve bellekte bir adresi ifade eden Address tipimizi ve array indislerinde kolayca kullanmamızı sağlayan Register tipimizi tanımlayalım:

pub type Address = u16;
pub type Register = usize;

Rust'ta yeni tipler type anahtar kelimesiyle tanımlanır. Başka bir tipi daha okunabilir bir hale getirmeye yarar. Bu sayede artık adres olduğunu bildiğimiz alanlar için u16 yerine Address tipini kullanabiliriz.

CHIP-8, 35 instructiona sahip bir yorumlayıcı. Bu OPCODE'ların hepsini bir enum içerisinde şu şekilde tanımlayabiliriz:

pub enum Instruction {
    /// 0x00E0 CLS: Ekranı temizler
    ClearDisplay,

    /// 0x00EE RET: Alt programdan döner
    Return,

    /// 0x1nnn JP: `nnn` adresine zıplar
    Jump(Address),

    /// 0x2nnn CALL: `nnn` adresindeki alt programı çağırır
    Call(Address),

    /// 0x3xnn SE: Eğer `x` registeri `nn`'e eşitse bir sonraki instruction'ı atlar
    SkipIfEqualsByte(Register, u8),

    /// 0x4xnn SE: Eğer `x` registeri `nn`'e eşit değilse bir sonraki instruction'ı atlar
    SkipIfNotEqualsByte(Register, u8),

    /// 0x5xy0 SE: Eğer `x` registeri `y` registerine eşitse
    /// bir sonraki instruction'ı atlar
    SkipIfEqual(Register, Register),

    /// 0x6xnn LD: `x` registerinin değerini `nn` yapar.
    LoadByte(Register, u8),

    /// 0x7xnn ADD: `x` registerindeki değere `nn` ekler.
    AddByte(Register, u8),

    /// 0x8xy0 LD: `x` registerinin değerini `y` registerinin değerine eşitler.
    Move(Register, Register),

    /// 0x8xy1 OR: `x` registerinin değerini `y` registerinin değeriyle
    /// bitwise OR işlemi yapar.
    Or(Register, Register),

    /// 0x8xy2 AND: `x` registerinin değerini `y` registerinin değeriyle
    /// bitwise AND işlemi yapar.
    And(Register, Register),

    /// 0x8xy3 XOR: `x` registerinin değerini `y` registerinin değeriyle
    /// bitwise XOR işlemi yapar.
    Xor(Register, Register),

    /// 0x8xy4 ADD: `x` registerinin değerini `y` registerinin değeriyle toplar ve `x`
    /// registerinin değerini sonuca eşitler. Çıkan sonuç 8 bitten fazla ise, `F` (carry)
    /// registerinin değerini 1 yapar, değilse 0.
    Add(Register, Register),

    /// 0x8xy5 SUB: `x` registerinin değerini `y` registerinin değerinden çıkarır ve `x`
    /// registerinin değerini sonuca eşitler. Eğer `x` registerindei değer, `y`
    /// registerindeki değerden büyükse, `F` (carry) registerinin değerini 1 yapar,
    /// değilse 0.
    Sub(Register, Register),

    /// 0x8xy6 SHR: `x` registerindeki değeri bir bit sağa kaydırır.
    /// Eğer `x` registerinin son biti 1 ise `F` (carry) registerinin değerini 1 yapar
    /// değilse 0.
    ShiftRight(Register),

    /// 0x8xy7 SUB: `y` registerinin değerini `x` registerinin değerinden çıkarır ve `x`
    /// registerinin değerini sonuca eşitler. Eğer `x` registerindei değer, `y`
    /// registerindeki değerden büyükse, `F` (carry) registerinin değerini 1 yapar,
    /// değilse 0.
    ReverseSub(Register, Register),

    /// 0x8xyE SHR: `x` registerindeki değeri bir bit sola kaydırır.
    /// Eğer `x` registerinin son biti 1 ise `F` (carry) registerinin değerini 1 yapar
    /// değilse 0.
    ShiftLeft(Register),

    /// 0x9xy0 SE: Eğer `x` registeri `y` registerine eşit değilse
    /// bir sonraki instruction'ı atlar
    SkipIfNotEqual(Register, Register),

    /// 0xAnnn LD: `I` registerinin değerini `nnn` yapar.
    LoadI(Address),

    /// 0xBnnn JP: `nnn` ve `V0` registerinin toplamından çıkan sonuca zıplar.
    JumpPlusZero(Address),

    /// 0xCxnn RND: Rastgele üretilen 8 bitlik sayı `nn` ile AND işleminden sonra
    /// çıkan sonuç `x` registerine atanır.
    Random(Register, u8),

    /// 0xDxyn DRW: `x` ve `y` registerinden başayarak `n` adet byte sprite'ı ekranda
    /// gösterir. Çakışma (collision) durumu `F` registerinde tutulur.
    Draw(Register, Register, u8),

    /// 0xEx9E SKP: `x` registerinde yer alan tuş basılırsa
    /// bir sonraki instruction'ı atlar
    SkipIfPressed(Register),

    /// 0xExA1 SKP: `x` registerinde yer alan tuş basılı değilse
    /// bir sonraki instruction'ı atlar
    SkipIfNotPressed(Register),

    /// 0xFx07 LD: `x` registerinin değerini delay timer yapar.
    LoadDelayTimer(Register),

    /// 0xFx0A LD: Bir tuşa basılmasını bekler ve basılan tuşun değerini `x`
    /// registerine atar. Tuş basılana kadar tüm çalıştırma durur.
    WaitForKeyPress(Register),

    /// 0xFx15 LD: Delay timer'ı `x` registerindeki değer yapar.
    SetDelayTimer(Register),

    /// 0xFx18 LD: Sound timer'ı `x` registerindeki değer yapar.
    SetSoundTimer(Register),

    /// 0xFx1E ADD: `I` registerinin değerini `I` ve `x` registerinin toplamı yapar.
    AddToI(Register),

    /// 0xFx29 LD: `I` registerinin değerini `x` registerinde yer alan değerden
    /// gelen sprite yeri yapar.
    LoadSprite(Register),

    /// 0xFx33 LD: `x` registerinin BCD (Binary Coded Decimal) cinsinden değerini:
    /// `I`, `I + 1`, `I + 2` alanlarında saklar.
    BCDRepresentation(Register),

    /// 0xFx55 LD: `I` registerinde yer alan alandan itibaren
    /// `0` dan `x` registerine kadar olan değerleri belleğe kopyalar.
    StoreRegisters(Register),

    /// 0xFx65 LD: `I` registerinde yer alan alandan itibaren
    /// bellekte yer alan değerleri `0` dan `x` registerine kopyalar.
    LoadRegisters(Register),
}

enum yapımızda içerisinde hiç bir veri tutmayan unit cinsinden ve veri barındıran tuple cinsinden bileşenleri görebilirsiniz. enum elemanları yukarıda da belirttiğimiz gibi bu iki türden de olabilir.

OPCODE'un Instruction'a Dönüştürülmesi

Rust'ta struct'lara olduğu gibi, enum'lara da metod ekleyebilirsiniz. Elimizde sayı halinde bulunan raw OPCODE'dan yeni bir Instruction instance'ı oluşturmak için new metodunu ekleyelim. Rust'ta yeni bir instance oluşturan metodlar genelde new ismiyle adlandırılır. Bu bir zorunluluk değil, istediğiniz ismi koyabilirsiniz.

impl Instruction {
    pub fn new<T: Into<Opcode>>(opcode: T) -> Option<Instruction> {
        let opcode = opcode.into();
        match opcode.0 & 0xF000 {
            0x0000 => match opcode.ooon() {
                0x0000 => Some(Instruction::ClearDisplay),
                0x000E => Some(Instruction::Return),
                _ => None,
            },
            0x1000 => Some(Instruction::Jump(opcode.onnn())),
            0x2000 => Some(Instruction::Call(opcode.onnn())),
            0x3000 => Some(Instruction::SkipIfEqualsByte(opcode.oxoo(), opcode.oonn())),
            0x4000 => Some(Instruction::SkipIfNotEqualsByte(
                opcode.oxoo(),
                opcode.oonn(),
            )),
            0x5000 => Some(Instruction::SkipIfEqual(opcode.oxoo(), opcode.ooyo())),
            0x6000 => Some(Instruction::LoadByte(opcode.oxoo(), opcode.oonn())),
            0x7000 => Some(Instruction::AddByte(opcode.oxoo(), opcode.oonn())),
            0x8000 => match opcode.ooon() {
                0x0000 => Some(Instruction::Move(opcode.oxoo(), opcode.ooyo())),
                0x0001 => Some(Instruction::Or(opcode.oxoo(), opcode.ooyo())),
                0x0002 => Some(Instruction::And(opcode.oxoo(), opcode.ooyo())),
                0x0003 => Some(Instruction::Xor(opcode.oxoo(), opcode.ooyo())),
                0x0004 => Some(Instruction::Add(opcode.oxoo(), opcode.ooyo())),
                0x0005 => Some(Instruction::Sub(opcode.oxoo(), opcode.ooyo())),
                0x0006 => Some(Instruction::ShiftRight(opcode.oxoo())),
                0x0007 => Some(Instruction::ReverseSub(opcode.oxoo(), opcode.ooyo())),
                0x000E => Some(Instruction::ShiftLeft(opcode.oxoo())),
                _ => None,
            },
            0x9000 => Some(Instruction::SkipIfNotEqual(opcode.oxoo(), opcode.ooyo())),
            0xA000 => Some(Instruction::LoadI(opcode.onnn())),
            0xB000 => Some(Instruction::JumpPlusZero(opcode.onnn())),
            0xC000 => Some(Instruction::Random(opcode.oxoo(), opcode.oonn())),
            0xD000 => Some(Instruction::Draw(
                opcode.oxoo(),
                opcode.ooyo(),
                opcode.ooon(),
            )),
            0xE000 => match opcode.oonn() {
                0x009E => Some(Instruction::SkipIfPressed(opcode.oxoo())),
                0x00A1 => Some(Instruction::SkipIfNotPressed(opcode.oxoo())),
                _ => None,
            },
            0xF000 => match opcode.oonn() {
                0x0007 => Some(Instruction::LoadDelayTimer(opcode.oxoo())),
                0x000A => Some(Instruction::WaitForKeyPress(opcode.oxoo())),
                0x0015 => Some(Instruction::SetDelayTimer(opcode.oxoo())),
                0x0018 => Some(Instruction::SetSoundTimer(opcode.oxoo())),
                0x001E => Some(Instruction::AddToI(opcode.oxoo())),
                0x0029 => Some(Instruction::LoadSprite(opcode.oxoo())),
                0x0033 => Some(Instruction::BCDRepresentation(opcode.oxoo())),
                0x0055 => Some(Instruction::StoreRegisters(opcode.oxoo())),
                0x0065 => Some(Instruction::LoadRegisters(opcode.oxoo())),
                _ => None,
            },
            _ => None,
        }
    }
}

Bu metodda Rust'a ait bir çok özellik bulunuyor. Öncelikle metod imzamızı inceleyelim:

    pub fn new<T: Into<Opcode>>(opcode: T) -> Option<Instruction> {

T tahmin edebileceğiniz gibi bir genelleyici (generic). Genelleyici tanımları metod adından sonra <> içerisinde yapılır. Genelleyicimiz Into<Opcode> özelliğine sahip bir parametre anlamına gelmektedir. Daha önce OPCODE'a eklediğimiz From özelliği sayesinde, opcode isiminli parametremiz, Opcode'a dönüştürülebilen herhangi bir tip olabilir. Bu sayede bu metodu istersek 16-bitlik bir sayı olarak da çalıştırabiliriz (Instruction::new(0xF155) gibi). Opcode'a eklediğimiz From özelliği, sayının otomatikmen Opcode tipine çevrilmesini sağlayacaktır.

Metodumuz aynı zamanda normal bir Instruction yerine Option<Instruction> dönmekte. Rust'ta yer alan Option tipi; opsiyonel bir değeri temsil etmektedir. Bu tip herhangi bir Some ya da hiç bir None değer taşıyabilir. CHIP-8'de sadece 35 OPCODE bulunduğundan genen raw OPCODE, Instruction tipine dönüştürülürken bilinen OPCODE'lar için Some(Instruction), bilinmeyenler için hiç bir değeri olan None dönüyoruz.

Metodumuz içerisinde yer alan let opcode = opcode.into(); satırı, yukarıda bahsettiğimiz genelleyici ile gelen Into<Opcode> özelliğine sahip opcode parametresini Opcode'a çevirmeye yarar. Aynı zamanda Rust gölgelemeye de izin verdiğinden, opcode değişkeni bu satırdan sonra Opcode tipine dönüşür.

Ardından gelen kod bloğunda raw OPCODE parçalanarak, okunabilir tipimiz olan Instruction'a dönüştürülüyor. Bu işlemi yaparken yine Rust'ın yine en önemli özelliklerinden biri olan pattern matching kullanıyoruz. match C de yer alan switch-case'e çok benzemesine rağmen, match edilen değerin tüm elemanlarını kapsamak zorundadır. Biz bu işlemi yaparken 16-bitlik bir sayı kullandığımızdan, işimize yarayan tüm değerleri aldık ve geri kalan ve işimize yaramayanlar içinde _ elemanını kullandık.

match bloğumuz Option<Instruction> döndüğü sürece, iç içe istediğimiz kadar match kullanabiliriz. Bu nedenle önce en soldaki nibble kontrol edildikten sonra, aynı nibble ile başlayan OPCODE'lar tekrar match ile kontrol edildip, OPCODE'a uyan bir Instruction tipi oluşturuluyor.

Instruction tipimizi içerisinde veri barındıran (daha önce tanımladığımız Address, Register vb.) bileşenler barındırdığından, Instruction tipi oluşturulurken bu bileşenlere gerekli değerler atanır. Bu işlemi yaparken daha önce Opcode tipine eklediğimiz yardımcı fonksiyonları kullanıyoruz.

Rust'ta her satır aynı zamanda bir deyim olduğundan ve match bloğumuz da aynı zamanda Option<Instruction> döndüğünden, return anahtar kelimesini kullanmamıza gerek yok. Deyim olarak kullanılan bu match bloğunun sonunda ; olmamasına dikkat edin.

Son olarak Rust'ta yorum satırları // ile başlar. Rust içerisinde çok gelişmiş bir belgeleme aracı (rustdoc) da bulundurmaktadır. Herhangi bir tanımdan önce (bu bir metod, fonksiyon, struct, bileşen ya da alan olabilir); 3 slash ile (///) oluşturacağınız yorum satırı, belgeleme aracı ile oluşturacağınız belgede o alan için tanımlama yapar. Kodlarken yazabileceğiniz bu yorum satırları, aynı zamanda herhangi bir Rust kütüphanesinin belgelemesini de çok kolay bir hale getirir.