Opcode Yapısı

CHIP-8'in tüm OPCODE'ları 16-bit uzunluğundadır. Bu nedenle çok basit bir bir OPCODE yapısı tanımlayalım:

pub struct Opcode(u16);

Rust'ta yapılar struct anahtar kelimesiyle tanımlanır. Struct'lar:

  • Opcode yapımızda kullandığımız gibi bir isim sahibi tuple olabilir.
  • C dilindeki gibi elemanlar içerebilir.
  • Hiç bir eleman içermeden unit biçiminde olabilir.

Biz sadece 16-bitlik bir sayı üzerine eklemeler yapacağımız için, tuple cinsinden struct kullanıyoruz.

Yapımızın başına gelen pub anahtar kelimesi, yapıyı açık hale getirerek şu an kullandığımız modül dışında da erişimini mümkün kılmaktadır. pub anahtar kelimesi yapılar dışında; enumlar, metodlar, fonksiyonlar ve type ile kendi tanımladığınız tipler için de kullanılabilir.

Rust'ta yer alan primitif tipler (yapımızda kullandığımız u16), tek haneli bir önek ardından gelen boyut ile tanımlanır. Yani u8: 8-bit boyutunda bir unsigned (sadece pozitif sayıları tutabilen) bir sayı tipidir. u16 tahmin edilebileceği üzere 16-bit boyutunda bir unsigned sayı tipidir. Dilde yer alan diğer primitif tipleri görmek için, Rust kitabında yer alan primitif tipler bölümüne başvurulabilir.

16-bitlik bu OPCODE'un her bir basamağı (nibble) farklı anlamlara gelmektedir. Örneğin: 0x71AA OPCODE'u; 1. registerdeki değere 0xAA sayısını eklemeye yarar. Bu nedenle her bir basamağı alabilmek için, Opcode yapımıza yardımcı metodları ekleyelim:

impl Opcode {
    /// OPCODE üzerinde 0x0X00 değerini döner
    fn oxoo(&self) -> usize {
        ((self.0 & 0x0F00) >> 8) as usize
    }

    /// OPCODE üzerinde 0x00Y0 değerini döner
    fn ooyo(&self) -> usize {
        ((self.0 & 0x00F0) >> 4) as usize
    }

    /// OPCODE üzerinde 0x000N değerini döner
    fn ooon(&self) -> u8 {
        (self.0 & 0x000F) as u8
    }

    /// OPCODE üzerinde 0x00NN değerini döner
    fn oonn(&self) -> u8 {
        (self.0 & 0x00FF) as u8
    }

    /// OPCODE üzerinde 0x0NNN değerini döner
    fn onnn(&self) -> u16 {
        self.0 & 0x0FFF
    }
}

Rust'ta yapılara eklenecek metodlar impl bloğu içinde yer alır. Aynı zamanda metodların ilk parametresi: &self bu fonksiyonların sadece oluşturulan bir Opcode instance'ı ile çalışacağını belirtir.

Methodlar fonksiyonlar gibidir ve fn anahtar kelimesi kullanılarak tanımlanır. Parametreler, dönüş değeri ve çağrıldığında çalıştırılacak kod bloğuna sahiptirler. Methodların fonksiyonlardan farkı ise, bir yapı (struct veya enum) için tanımlanırlar ve ilk parametre o yapıya ait instance'a ulaşabileceğimiz self anahtar kelimesidir. Eğer metod çalıştırıldığında, instance sahipliğini almak istemiyorsak, self parametresi başına & koyularak referans olarak eklenir. Instance'ı referans olarak sunmayıp sahipliğini alsaydık, bu metodların kullanımının ardından instance sahipliği kaybolacaktı.

Metod adı ve parametrelerinin ardından döneceği tip -> dan sonra yazılır.

Yapımız tuple cinsinden bir struct olduğu için, yapı içerisindeki veriye aynı tuplelarda olduğu gibi self.0 ile ulaşılır.

Rust'ta primitif tipler (u8, u16 vb.) as anahtar kelimesiyle birbirine dönüştürüleblir. Fakat unutmayın ki boyutu büyük bir sayı (u16), as anahtar kelimesiyle daha küçük bir sayıya dönüştürülürken hiç bir uyarı vermeden sayıyı kırpabilir. Bu nedenle eğer büyük bir sayıyı küçük bir sayıya dönüştürmek istiyorsanız u8::from() metodunu kullanın. Biz geliştirdiğimiz uygulamada dönüştürülen sayıların 16-bit'ten küçük olduğunu bildiğimiz için as anahtar kelimesini kullanacağız.

Metod isimleri ilk bakışta garip gelebilir fakat, CHIP-8 OPCODE'larında her zaman soldan ikinci basamak x: ilk register numarasını, üçüncü basamak ise y: ikinci register numarasını içermektedir. İleride array cinsinden olan registerlarda daha rahat çalışabilmek için x ve y registerlarını dönen oxoo, ooyo metodları usize dönmektedir (Rust'ta array indisleri usize cinsinden olmalıdır).

Yapılan bitwise işlemleri açıklamak gerekirse, tekrar 0x71AA örneğini ele alalım. 0x71AA sayısında ikinci basamakta yer alan 1 değerini almak için öncelikle oxoo metodunu kullanmamız gerekli. Bu metod şu işlemleri yapmaktadır:

0x71AA     111000110101010  AND
0x0F00     000111100000000
0x0100     000000100000000  >> 8
0x0001     000000000000001  # 16-bit uzunluğundaki sayı, CHIP-8'de sadece 16
--------------------------  # register bulunduğundan 8-bit'e dönüştürülür:
0x01              00000001

Ardından 0xAA sayısı alınabilmesi için oonn metodu kullanılır:

0x71AA     111000110101010  AND
0x00FF     000000011111111
0x00AA     000000010101010  # Son iki basamak sadece 8-bit uzunluğunda
                            # olabileceğinden çıkan sonuç 8-bit'e dönüştürülür
--------------------------
0xAA              10101010

Sayıların OPCODE'a Dönüştürülmesi

İleride işimize yarayacağı için 16-bitlik bir sayının (u16), Opcode yapısına kolayca dönüştürülmesini sağlayan From özelliğini eklememiz gerekli. Rust'ta yapılara yeni özellikle şu şekilde eklenir:

impl From<u16> for Opcode {
    fn from(opcode: u16) -> Opcode {
        Opcode(opcode)
    }
}

From özelliği aynı zamanda Into özelliğini de otomatikmen eklediğinden, Opcode kullanmamız gereken parametrelerde Into<Opcode> genelleyici tipini kullanmamız yeterli olacak. Bu sayede Opcode hem verbose olarak Opcode(0xF1AA) hem de into metoduyla direkt bir sayıdan dönüştürülebilir.

Genelleyiciler (generics) ve özellikler (traits) çok kapsamlı bir konudur ve Rust'ın en önemli bileşenlerindendir. Şimdilik kafanızı çok fazla karıştırmanıza gerek yok fakat isterseniz Rust kitabında yer alan: Özellikler bölümünü okuyabilirsiniz.