Форум программистов, компьютерный форум, киберфорум
loothood
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

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

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

(Исходную статью я разбил на две части. Оригинал)
Первую часть перевода можно почитать тут
Работа с памятью
Создание интерфейса с помощью 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 выглядит как самый лучший вариант на данный момент.

Код проекта на гитхаб
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
Программа опроса у.з. расходомера SLS-720F
Argus19 02.09.2026
Программа опроса у. з. расходомера SLS-720F Программа опрашивает один раз в минуту три ультразвуковых расходомера SLS-720F через интерфейс RS-485 по протоколу Modbus RTU. Опрашиваются регистры. . .
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
Архитектура биовида Стива в Майнкрафте: Зачем бонобо кубический каннибализм
anaschu 30.08.2026
Кубический Вагинокапитализм в Minecraft: Математический инвариант ОДУ и рок Стивов-бонобо Главная задача разработанной «Модели Всего» — наглядно продемонстрировать наличие системной «судьбы». . .
Оттачиваю умение писать js программы.
russiannick 30.08.2026
Проектом выходного дня стало написание Книги шифров Виженера. Итогом стала версия 200, синий туман. Синий туман назван так, потому что замораживает текст под собой. Нажатие синих кнопок управляют. . .
мат медиц модель 30. презентация проекта
anaschu 27.08.2026
хоп хоп хоп хидахоп, а я кладую))
Как у меня протекала болезнь
zorxor 27.08.2026
Здравствуйте, друзья! Эта запись блога предназначена именно для вас - для моих дорогих друзей, которые знали меня лично. Чтобы ответить на вопрос - а что же со мной произошло на самом деле? Я учился. . .
Нашел вот забавное видео о измерениях. Лучшее что я видел на эту тему
kumehtar 26.08.2026
ILETXiw9bMQ Основная суть и тезисы по измерениям: 0D (Нулевое измерение): точка, не имеющая длины, ширины, высоты или объема. Объект не может перемещаться в 0D. 1D (Первое измерение):. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru