Форум программистов, компьютерный форум, киберфорум
Наши страницы
loothood
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Пишем CHIP-8 эмулятор с использованием Rust и WebAssembly (часть 2)

Запись от loothood размещена 25.12.2017 в 23:11

(Исходную статью я разбил на две части. Оригинал)
Первую часть перевода можно почитать тут
Работа с памятью
Создание интерфейса с помощью WebAssembly может быть довольно сложным. WebAssembly поддерживает только четыре базовых типа, поэтому передача чего-либо более сложного на границе WebAssembly/JavaScript требует некоторого кодирования/декодирования.

WebAssembly и JavaScript работают с общей памятью, к которой оба имеют на чтение и изменение. Данные могут быть общими через общие ячейки памяти.

Например, у эмулятора CHIP-8 всего 4Кб памяти, которая определена структурами и экспортируется следующим способом:
C
1
2
3
4
5
6
7
8
9
10
11
static mut CPU: Cpu = Cpu {
    memory: [0; 4096],
    // other fields go here …
}
 
#[no_mangle]
pub fn get_memory() -> &'static [u8; 4096] {
    unsafe {
        &CPU.memory
    }
}
Во время выполнения экземпляр CPU будет размещен где-то в памяти модуля WebAssembly. Мы можем узнать где именно, используя функцию get_memory.

На стороне JavaScript, мы можем получить доступ к этой памяти с помощью exports.memory. С помощью функции get_memory, к 4Кб памяти CHIP-8 можно получить доступ так:
C
1
2
3
4
5
6
7
8
const instance = await WebAssembly.instantiate(module);
const exports = instance.exports;
 
const programMemory = new Uint8Array(
  exports.memory.buffer,
  exports.get_memory(),
  4096
);
Когда я запускаю код выше, get_memory возвращает значение 16760, в котором компилятор Rust решил выделить эту структуру.

Загрузка ROM в память так же проста. Сброс CPU (сбрасывает регистры и т.д.), а затем происходит запись в массив programMemory:
C
1
2
3
4
5
6
7
8
9
fetch(`roms/${rom}`)
  .then(i => i.arrayBuffer())
  .then(buffer => {
    const rom = new DataView(buffer, 0, buffer.byteLength);
    exports.reset();
    for (i = 0; i < rom.byteLength; i++) {
      programMemory[0x200 + i] = rom.getUint8(i);
    }
  });
Обратите внимание на смещение 0х200 - оно связано с загрузкой ROM с 512-байтовым смещением.

Чтение дисплея или регистра значений использует тот же подход, что и выше.


Немного Rust
Я новичок в Rust(около двух недель), потому я не привожу много своего кода. Боюсь показаться глупым из-за очевидных ошибок.

Мой опыт использования Rust очень вдохновляет. Язык гораздо более строгий, чем те языки что я использовал до этого. Это другой конец палки, если сравнивать его с JavaScript. Кроме того, Rust современен и элегантен. По-моему, самая сложная концепция, к которой придется привыкнуть - это концепция владения Rust. Я работал с несколькими методами управления памятью (сборщик мусора, автоматический подсчет ссылок), однако концепция владения Rust, где у вас есть единственное изменяемое связывание с ресурсом, нова для меня.

Часть эмулятора CHIP-8, которая по-моему получилось особенно хорошо, это метод execute_opcode. Инструкции CHIP-8 имеют длину 16 бит, как описано в этой технической справочной информации.

Для некоторых инструкций первый полубайт (4 бита) представляет код операции, а остальные полубайты кодируют данные кода операции. Например, операция «jump» обновляют счетчик программы с определенным адресом. Он кодируется как 0x1nnn, где первое значение 0x1 кодирует операцию перехода, а следующие три(nnn) - представляют адрес.

Сопоставление шаблонов с диапазонами в Rust - идеальный инструмент для реализации этого. Вот пример, который использует соответствие диапазонов для реализации инструкции перехода:
C
1
2
3
4
5
6
7
fn process_opcode(&mut self, opcode: u16) {
    match (opcode) {
        0x1000 ... 0x1FFF => self.pc = opcode & 0x0FFF,
        // match other ranges to process other opcodes.
        _ => ()
    }
}
К сожалению, не все инструкции могут быть разделены на основе первого полубайта. Например, есть девять различных инструкций, начинающихся с 0x8, что приводит к вложенному сопоставлению:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn process_opcode(&mut self, opcode: u16) {
    match (opcode) {
        0x1000 ... 0x1FFF => self.pc = opcode & 0x0FFF,
        // ...
        0x8000 ... 0x8FFF => {
            match opcode & 0x00F {
                // LD Vx, Vy
                0x0 => self.v[x] = self.v[y],
                // OR Vx, Vy
                0x1 => self.v[x] = self.v[x] | self.v[y],
                // …
             }
        // ...
        _ => ()
    }
}
Итак, чтобы обработать инструкции, мы должны иметь возможность сопоставлять коды в разных местах. С Rust это возможно, создавая кортеж, который разбивает код операции на четыре полубайта, а затем сопоставляя эти компоненты отдельно. Ниже приведен пример некоторых команд:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// break up into nibbles
let op_1 = (opcode & 0xF000) >> 12;
let op_2 = (opcode & 0x0F00) >> 8;
let op_3 = (opcode & 0x00F0) >> 4;
let op_4 = opcode & 0x000F;
 
match (op_1, op_2, op_3, op_4) {
    // CLS
    (0, 0, 0xE, 0) => self.display.cls(),
    // JP
    (0x1, _, _, _) => self.pc = nnn,
    // LD Vx, Vy
    (0x8, _, _, 0x0) => self.v[x] = self.v[y],
    // OR Vx, Vy
    (0x8, _, _, 0x1) => self.v[x] = self.v[x] | self.v[y],
    // AND Vx, Vy
    (0x8, _, _, 0x2) => self.v[x] = self.v[x] & self.v[y],
    // LD F, Vx
    (0xF, _, 0x2, 0x9) => self.i = vx as u16 * 5,
    // ...
    (_, _, _, _) => ()
}
Заметьте, я мог бы использовать деструктуризацию, чтобы вытащить другие компоненты каждой команды, например:
C
1
(0x8, x, y, 0x0) => self.v[x as usize] = self.v[y as usize]
Однако, данный подход может осложнить код, потому я извлекаю большинство типичных шаблонов кода операций перед выражением соответствия:
C
1
2
3
4
5
6
7
let x = ((opcode & 0x0F00) >> 8) as usize;
let y = ((opcode & 0x00F0) >> 4) as usize;
let vx = self.v[x];
let vy = self.v[y];
let nnn = opcode & 0x0FFF;
let kk = (opcode & 0x00FF) as u8;
let n = (opcode & 0x000F) as u8;

Управление зависимостями
Rust упаковывает в пакет(crates) повторно используемые компоненты, которыми управляет cargo. Это очень похоже на npm, CocoaPods и другие менеджеры пакетов.

В моем проекте было несколько пакетов(crates), которые полезны для использования: - rand- генерация случайных чисел. - lazy_static - в Rust вы не можете вызвать функцию, чтобы построить статическое значение, поэтому вы не можете использовать функцию-конструктор. Ленивый статический макрос позволяет вам инициализировать статические переменные лениво, избегая этого ограничения.

К сожалению, я столкнулся с трудностями при интеграции этих пакетов в мой проект. Пакеты имеют зависимость от платформы, потому их пришлось доработать чтобы подружить их с WebAssembly. Я нашел форк с некоторыми необходимыми изменениями, но изменения не все, которые были мне необходимы.

В моем случае, я просто взял реализацию случайных чисел и скопировал в ской код и отказался от ленивой инициализации.


Заключение
Я в самом деле впечатлен языком Rust. Уверен что WebAssembly - отличная возможность по привлечению программистов для этого языка. В краткосрочной перспективе выбор языка для WebAssembly немного ограничен. Для веб разработчиков, которые хотят попробовать что-то новое, Rust выглядит как самый лучший вариант на данный момент.

Код проекта на гитхаб
Размещено в Без категории
Просмотров 208 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru