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.