Здравствуйте. Меня зовут Владислав, и я хочу рассказать о своем экспериментальном проекте на ранней стадии. Называется он WIE (Wie Is Emulator), что отсылает к великим.

WIE — это исследовательский эмулятор пользовательского режима (userspace) для запуска 64-битных Windows‑приложений (PE64) на архитектуре macOS Apple Silicon. Проект написан на Rust 1.97, а в качестве бэкенда компиляции используется Cranelift для трансляции x86-64 инструкций на лету в нативный ARM64.

Главный Proof of Concept на сегодня: эмулятор успешно крутит под честным JIT реальный консольный Windows 7-Zip (7za.exe), выполняя сжатие и распаковку с полным совпадением SHA-256 хэшей. А также для тестирования было разработано более 20 тестовых EXE файлов на различные задачи: От математики и долгих циклов, заканчивая проверкой ввода и корректности многопоточности

Что не планируется

  • 32-битные и 16-битные приложения

  • Никакой полной истории от Windows от 95 до 11. Только Windows 10 и совместимые PE64 со старых версий

  • Мягкая трансляция памяти (Soft‑translate): принципиально не использую Wine‑style identity mapping (mmap(addr = guest_va)). Гостевые адреса изолированы и всегда транслируются через софтверные таблицы регионов, арены, структуры VAD и PageMap.

Использование ИИ

«Да ладно, на сайт пришел очередной вайбкодер!» — скажете вы, и в принципе я с вами соглашусь. Значительная часть кода проекта действительно сгенерирована нейросетями, и я этого не скрываю. Но должен сразу уточнить: этот проект не из тех, где «запустил 12 ИИ‑агентов на ночь и получил убийцу Wine».

Я сам очень не люблю подход, когда человек, вообще не разбирающийся в технологиях, слепо верит, что нейросеть создаст шедевр, пока он лежит на диване.

За время работы над разными приватным проектами я научился глубоко читать и понимать Rust: какие конструкции за что отвечают, как взаимодействовать с компилятором и банально как правильно подходить к работе с нейросетями. Ведь всем «нейрокодерам» известно — без жесткой и правильной инструкции ИИ моментально уйдет не туда.

Также я бьюсь за чистоту кода. Мне важно досконально понимать, что происходит в системе. Когда в проекте скапливается куча legacy‑мусора, контролировать логику становится тяжело не только мне, но и самой нейросети.

И, естественно, unsafe. Главный мрак Rust. Практически весь unsafe, который есть в WIE, локализован строго в блоке ядра CPU. Есть пара исключений в блоке WinAPI, но на этом всё. В остальном проект глобально настроен на безопасный код.

Надеюсь, я хоть минимально убедил вас в том, что я полностью контролирую архитектуру и слежу за проектом.

Начало конца

Лично мне никогда не приходилось мучаться с Wine, так как лично мне Windows приложения не нужны были. Я не геймер, так что главная причина использовать отпадает, ничего специфичного не надо было.

Но вот в один момент меня что‑то потянуло на ретроигры, а вернее довольно интересную сферу — Ромхакинг. А точнее Super Mario World и легендарный редактор Lunar Magic. Я когда начал изучать эту тему очень сильно удивился: Ради того чтобы переделать оригинальный ROM файл за примерно 2 десятилетия сообщество придумало такое количество способов расширить файл и настолько перерабатывать игру, что просто жуть.

Мне стало интересно как устроен Lunar Magic авторства FuSoYa, который является одним из главнейших столбов. Кратко: есть полно утилит на отдельную часть (музыка, графика, кастомные предметы) и они разрозненны, а Lunar является сборщиком упаковщиком с понятным графическим интерфейсом. Но есть деталь... Практически все ромхакерское ПО исключительно под Windows и только пара есть и на macOS. Конкретно Lunar у меня спокойно в Wine запустится, но другие у многих вызывают проблемы. Но сначала именно Lunar меня заинтересовал именно из‑за скрепления и понимания всего.

И я предпринял попытку нейросетевого реверс‑инжиниринга и портирования редактора на Rust... Что тут сказать: эта попытка хоть и неплохо начиналась, но с треском рухнула на этапе рендеринга. Чтобы вы понимали масштабы бедствия: более трех тысяч функций в EXE‑файле весом в несколько мегабайт.

Однако меня не отпускало. Тянуло попробовать запустить это на Mac без Wine. Я искал разные способы, но только один показался более‑менее реалистичным: написать заглушки под WinAPI, а сам гостевой EXE‑файл скормить в эмулятор Unicorn Engine. С этого всё и началось. Я генерировал и генерировал заглушки, которые просто пропускали выполнение по инициализации, но почти ничего общего с реальными функциями kernel32 и прочих библиотек не имели.

В какой‑то момент я подумал, что занимаюсь глупостью. То есть я трачу время (пусть код пишу и не я, но, как уже говорил, я жестко контролирую процесс генерации) и добавляю кучу заглушек ради одного нишевого приложения, которое лично мне в будущем никак не пригодится. После этого проект я забросил.

Но долго без дела сидеть не смог — не получалось найти занятие по душе. Мне хотелось создать что‑то уникальное, ведь рынок IT, как известно, перенасыщен стандартными решениями. Проверяя свои старые репозитории в попытке найти то, что можно реанимировать, я всё‑таки вернулся к этой идее. Но на этот раз я полностью поменял перспективу. Вместо попытки запустить один конкретный Lunar Magic я принял решение замахнуться на невозможное: переработать проект так, чтобы он мог запускать самые разные EXE‑файлы. По сути, сделать аналог Wine.

Но Wine — это не эмулятор! Это транслятор системных вызовов. А у тебя под капотом именно эмуляция процессора

Так и родилось название WIE — Wie Is Emulator.

Судьбоносная замена

Unicorn сначала казался неплохим. Он был удобен в плане анализа и просто проект изначально под него строился. Но теперь две проблемы которая перекрывает все хорошее: CPU и скорость.

Как‑бы ты не пытался снижать потребление CPU, ты всегда будешь больше тратить чем Wine. Я конечно начал попытку оптимизаций. В какой‑то момент ИИ предлагал варианты оптимизаций один из вариантов был переход на Cranelift + iced‑x86. Я заинтересовался, так как он написал что он легковеснее, JIT движок быстрее чем у Unicorn и вообще предназначен изначально под веб. Решил переходить на него.

Замена Unicorn на Cranelift правда‑сказать сожгла недельные лимиты. И даже не столько замена, сколько попытка ускорить и оптимизировать минимально. В какой то момент на тестах еще сохранившегося в проекте Lunar Magic, который уже проходил цикл инициализации полностью в один момент скорость cranelift превзошла Unicorn почти на 2 секунды.

Вообще кстати они оба со временем ускорялись все больше и больше. Начиналось вроде где‑то с 20 секунд, а закончилось от семи до десяти (если я не ошибаюсь). После этого я решил что пора вычеркивать lunar из проекта: Все переменные, адреса, функции подстроенные только под него, а также Unicorn Engine, который я успешно на Cranelift и iced‑x86. Вот после этого момента можно и сказать, что проект начал принимать облик нынешнего варианта.

А как устроено?

Если отбросить дальнейший путь разработки и посмотреть на WIE (Wie Is Emulator) сегодня, то это модульный, легковесный эмулятор пользовательского режима, написанный на Rust. Его архитектура разделена на четыре ключевых блока, которые работают в тесной связке:

  • wie‑pe (Загрузчик): Берет гостевой 64-битный Windows‑бинарник, парсит его структуру, маппит секции в изолированную хост‑память и полностью переписывает Таблицу импортов (IAT). Все системные вызовы Windows подменяются на кастомные виртуальные адреса‑ловушки.

  • wie‑winapi (Прослойка окружения): Та самая поверхность WinAPI, которую мы итеративно воссоздаем под нужды приложений. Чтобы не прыгать в контекст хоста по малейшему поводу, базовые и часто вызываемые функции (например, работа с ошибками вроде GetLastError или SetLastError) имеют инлайновые заглушки прямо в гостевой памяти.

  • wie‑cpu (Движок компиляции): Сердце проекта. С помощью iced-x86 декодируются инструкции x86-64, собираются в базовые блоки и передаются JIT‑бэкенду Cranelift, который на лету превращает их в нативный ARM64-код, оптимизированный под Neon‑векторы Apple Silicon. 

  • wie‑cli (Командный пункт): Место откуда идет управление, слежка, шпионаж... Проще говоря просто CLI блок с тремя командами. trace, run и inspect

Архитектура памяти

Почему Soft‑translate, а не подход Wine? Он использует подход Identity Mapping, когда гостевой адрес пытается напрямую отобразиться на аналогичный адрес хоста через mmap(addr = guest_va).

В WIE реализована мягкая трансляция памяти (Soft‑translate). Гостевое пространство полностью изолировано. Каждый адрес гостя — это виртуальная абстракция, которая принудительно транслируется через софтверные таблицы регионов, арены, структуры VAD (Virtual Address Descriptor) и PageMap. Да, это накладывает свои накладные расходы, но дает тотальный контроль над правами страниц и безопасностью выполнения. 

Многопоточность

Эмуляция многопоточности — это отдельная тема. В WIE гостевые потоки, создаваемые через CreateThread, маппятся на реальные системные потоки хоста (macOS pthreads) в соотношении 1:1. Для синхронизации используется глобальный мьютекс процессора (CpuEngine process mutex). Когда гостевой поток уходит в законное ожидание (например, через WaitForSingleObject), блокировка движка CPU освобождается, предотвращая холостой простой хост‑системы.

Конечно это все хорошо, но что в итоге? Что угодно запустит?

Нет! Это очень ранний прототип. Но это не значит что он ничего не умеет. Как говорил в процессе разработке было создано более 20 EXE. Далее будут примеры:

Пример 1: Цикл

#include <windows.h>

void entry(void) {
  volatile unsigned long long counter = 0;
  volatile unsigned long long limit = 100000000ULL;
  if (counter < limit) {
    do {
      volatile unsigned long long tmp = counter ^ 0xDEADBEEF;
      tmp = tmp * 3 + 1;
      (void)tmp;

      counter++;
    } while (counter < limit);
  }

  ExitProcess(0);
}

Это был первый крупный тест. До оптимизации он проходил за 9 секунд, теперь:

time ./target/release/wie-cli run micro-exes/out/long_loop.exe
run_micro: path=micro-exes/out/long_loop.exe
cpu_backend: jit
entry=0x0000000140001000 initial_rsp=0x000000002000eff8
events=1 termination=ExitProcess { code: 0 }
  [   0] KERNEL32.dll!ExitProcess handled=true ret=None
run_micro: ok exit=0
./target/release/wie-cli run micro-exes/out/long_loop.exe  0.31s user 0.01s system 99% cpu 0.325 total

Процессор на 99 процентах, но это потому‑что выполняется полезная работа. В тестах ожидания, процессор падает практически до нуля.

Пример 2: Тяжелая многопоточность

#include <windows.h>

#define WORKERS 4
#define ITERS   512

typedef LONG(WINAPI *PFN_Inc)(LONG volatile *);

static CRITICAL_SECTION g_cs;
static volatile LONG g_atomic = 0;
static volatile LONG g_under_cs = 0;
static volatile LONG g_slots[WORKERS];
static HANDLE g_start_event;
static HANDLE g_done_event;
static volatile LONG g_ready = 0;
static PFN_Inc g_inc;

static DWORD WINAPI worker(LPVOID param) {
    int id = (int)(ULONG_PTR)param;
    int i;
    HANDLE heap;
    LONG marker = 0x1000 + id;

    if (g_inc(&g_ready) == WORKERS) {
        SetEvent(g_done_event);
    }
    WaitForSingleObject(g_start_event, INFINITE);

    heap = GetProcessHeap();
    for (i = 0; i < ITERS; i++) {
        void *p;

        g_inc(&g_atomic);

        EnterCriticalSection(&g_cs);
        g_under_cs++;
        p = HeapAlloc(heap, 0, 64);
        if (p) {
            *((volatile LONG *)p) = marker;
            HeapFree(heap, 0, (LPVOID)p);
        }
        LeaveCriticalSection(&g_cs);

        g_slots[id] = marker;
    }

    ExitThread(0);
    return 0;
}

void entry(void) {
    HANDLE threads[WORKERS];
    DWORD wait;
    int i;
    HMODULE k;
    const LONG expect = (LONG)(WORKERS * ITERS);

    for (i = 0; i < WORKERS; i++) {
        g_slots[i] = 0;
    }

    k = GetModuleHandleA("KERNEL32.dll");
    if (!k) {
        k = GetModuleHandleA("kernel32.dll");
    }
    if (!k) {
        ExitProcess(6);
    }
    g_inc = (PFN_Inc)GetProcAddress(k, "InterlockedIncrement");
    if (!g_inc) {
        ExitProcess(6);
    }

    InitializeCriticalSection(&g_cs);
    g_start_event = CreateEventA(NULL, TRUE, FALSE, NULL); /* manual */
    g_done_event = CreateEventA(NULL, TRUE, FALSE, NULL);
    if (!g_start_event || !g_done_event) {
        ExitProcess(6);
    }

    for (i = 0; i < WORKERS; i++) {
        threads[i] = CreateThread(NULL, 0, worker, (LPVOID)(ULONG_PTR)i, 0, NULL);
        if (threads[i] == NULL || threads[i] == INVALID_HANDLE_VALUE) {
            ExitProcess(1);
        }
    }

    wait = WaitForSingleObject(g_done_event, 30000);
    if (wait != WAIT_OBJECT_0) {
        ExitProcess(6);
    }
    SetEvent(g_start_event);

    for (i = 0; i < WORKERS; i++) {
        wait = WaitForSingleObject(threads[i], INFINITE);
        if (wait != WAIT_OBJECT_0) {
            ExitProcess(2);
        }
        CloseHandle(threads[i]);
    }

    if (g_atomic != expect) {
        ExitProcess(3);
    }
    if (g_under_cs != expect) {
        ExitProcess(4);
    }
    for (i = 0; i < WORKERS; i++) {
        if (g_slots[i] != (LONG)(0x1000 + i)) {
            ExitProcess(5);
        }
    }

    DeleteCriticalSection(&g_cs);
    CloseHandle(g_start_event);
    CloseHandle(g_done_event);
    ExitProcess(0);
}

Результат:

time ./target/release/wie-cli run micro-exes/out/mt_stress.exe
run_micro: path=micro-exes/out/mt_stress.exe
cpu_backend: jit
entry=0x00000001400010e0 initial_rsp=0x000000002000eff8
events=17 termination=ExitProcess { code: 0 }
  [   0] kernel32.dll!getmodulehandlea handled=true ret=Some(1627389952)
  [   2] kernel32.dll!initializecriticalsection handled=true ret=Some(0)
  [   3] KERNEL32.dll!CreateEventA handled=true ret=Some(2147483649)
  [   4] KERNEL32.dll!CreateEventA handled=true ret=Some(2147483650)
  [   5] KERNEL32.dll!CreateThread handled=true ret=Some(2147483651)
  [   6] KERNEL32.dll!CreateThread handled=true ret=Some(2147483652)
  [   7] KERNEL32.dll!CreateThread handled=true ret=Some(2147483653)
  [   8] KERNEL32.dll!CreateThread handled=true ret=Some(2147483654)
  [  10] KERNEL32.dll!SetEvent handled=true ret=Some(1)
  [  12] kernel32.dll!closehandle handled=true ret=Some(1)
  [  14] kernel32.dll!closehandle handled=true ret=Some(1)
  [  16] kernel32.dll!closehandle handled=true ret=Some(1)
  [  18] kernel32.dll!closehandle handled=true ret=Some(1)
  [  19] kernel32.dll!deletecriticalsection handled=true ret=Some(0)
  [  20] kernel32.dll!closehandle handled=true ret=Some(1)
  [  21] kernel32.dll!closehandle handled=true ret=Some(1)
  [  22] KERNEL32.dll!ExitProcess handled=true ret=None
run_micro: ok exit=0
./target/release/wie-cli run micro-exes/out/mt_stress.exe  0.05s user 0.07s system 163% cpu 0.074 total

Как вы видите: 163% CPU, то есть больше одного потока, 74 миллисекунды всего, а exit 0

Пример 3: Запуск CLI версии 7zip

Предупреждение: Поверхность WinAPI для 7-Zip огромна, и проект находится в стадии наполнения, поэтому проверен далеко не весь функционал утилиты. Для демонстрации мы берем оригинальный, немодифицированный консольный Windows‑бинарник 7za.exe (из официального пакета 7-Zip Extra x64) и запускаем его в изолированном окружении («бутылке»). На данный момент эмулятор стабильно запускает пять команд:

  • a (сжатие) — упаковка файлов в формат.7z, проверенная как в один поток, так и в многопоточных режимах (-mmt2 / -mmt4).

  • x(распаковка) — извлечение файлов с полным сохранением путей.

  • l (листинг) — чтение структуры и вывод содержимого архива.

  • i (инвентаризация) — проверка доступных системе кодеков и хэшеров.

  • help — вывод списка команд.

Finished `release` profile [optimized] target(s) in 0.05s
blob.bin 262144
zsh: command not found: #
bottle_root: /var/folders/16/y3g8vpb14db0rg6g632rl1_m0000gn/T//wie-7za-bottle-71495
guest_args: ["--help"]

7-Zip (a) 26.02 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-06-25

Usage: 7za <command> [<switches>...] <archive_name> [<file_names>...] [@listfile]

<Commands>
  a : Add files to archive
  b : Benchmark
  d : Delete files from archive
  e : Extract files from archive (without using directory names)
  h : Calculate hash values for files
  i : Show information about supported formats
  l : List contents of archive
  rn : Rename files in archive
  t : Test integrity of archive
  u : Update files to archive
  x : eXtract files with full paths

<Switches>
  -- : Stop switches and @listfile parsing
  -ai[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : Include archives
  -ax[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : eXclude archives
  -ao{a|s|t|u} : set Overwrite mode
  -an : disable archive_name field
  -bb[0-3] : set output log level
  -bd : disable progress indicator
  -bs{o|e|p}{0|1|2} : set output stream for output/error/progress line
  -bt : show execution time statistics
  -i[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : Include filenames
  -m{Parameters} : set compression Method
    -mmt[N] : set number of CPU threads
    -mx[N] : set compression level: -mx1 (fastest) ... -mx9 (ultra)
  -o{Directory} : set Output directory
  -p{Password} : set Password
  -r[-|0] : Recurse subdirectories for name search
  -sa{a|e|s} : set Archive name mode
  -scc{UTF-8|WIN|DOS} : set charset for console input/output
  -scs{UTF-8|UTF-16LE|UTF-16BE|WIN|DOS|{id}} : set charset for list files
  -scrc[CRC32|CRC64|SHA256|SHA1|XXH64|*] : set hash function for x, e, h commands
  -sdel : delete files after compression
  -seml[.] : send archive by email
  -sfx[{name}] : Create SFX archive
  -si[{name}] : read data from stdin
  -slp : set Large Pages mode
  -slt : show technical information for l (List) command
  -snh : store hard links as links
  -snl : store symbolic links as links
  -sni : store NT security information
  -sns[-] : store NTFS alternate streams
  -so : write data to stdout
  -spd : disable wildcard matching for file names
  -spe : eliminate duplication of root folder for extract command
  -spf[2] : use fully qualified file paths
  -ssc[-] : set sensitive case mode
  -sse : stop archive creating, if it can't open some input file
  -ssp : do not change Last Access Time of source files while archiving
  -ssw : compress shared files
  -stl : set archive timestamp from the most recently modified file
  -stm{HexMask} : set CPU thread affinity mask (hexadecimal number)
  -stx{Type} : exclude archive type
  -t{Type} : Set type of archive
  -u[-][p#][q#][r#][x#][y#][z#][!newArchiveName] : Update options
  -v{Size}[b|k|m|g] : Create volumes
  -w[{path}] : assign Work directory. Empty path means a temporary directory
  -x[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : eXclude filenames
  -y : assume Yes on all queries
run_micro: path=real_exes/7za.exe
cpu_backend: jit
entry=0x00000000004e9ac0 initial_rsp=0x000000002000eff8
events=243 termination=ExitProcess { code: 0 }
run_micro: ok exit=0
bottle_root: /var/folders/16/y3g8vpb14db0rg6g632rl1_m0000gn/T//wie-7za-bottle-71495
guest_args: ["a", "-mmt2", "-bd", "C:\\App\\blob.7z", "C:\\App\\blob.bin"]

7-Zip (a) 26.02 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-06-25

Scanning the drive:
1 file, 262144 bytes (256 KiB)

Creating archive: C:\App\blob.7z

Add new data to archive: 1 file, 262144 bytes (256 KiB)


Files read from disk: 1
Archive size: 479 bytes (1 KiB)
Everything is Ok
run_micro: path=real_exes/7za.exe
cpu_backend: jit
entry=0x00000000004e9ac0 initial_rsp=0x000000002000eff8
events=3323 termination=ExitProcess { code: 0 }

Будущее проекта и о багах

Как вы знаете, в любом крупном проекте всегда есть баги: явные, скрытые и архитектурные. А при активном использовании нейросетей шанс поймать неочевидную проблему возрастает в разы. Я уверен, что по мере дальнейшей разработки и увеличения покрытия WinAPI обязательно найдутся новые скрытые проблемы в цепочках JIT или синхронизации потоков, но тем интереснее будет их искать и исправлять.

Проект всё еще находится на очень ранней стадии и ни в коем случае не претендует на статус полноценной «замены Wine», но как исследовательский прототип, доказывающий жизнеспособность концепции, я считаю, что он полностью удался.

В заключение хочу добавить немного личного контекста. Мне 15 лет, и WIE — это мой сугубо исследовательский pet‑проект. Я создал его для того, чтобы глубоко, «руками» прочувствовать системное программирование, разобраться в устройстве JIT‑компиляторов, Cranelift, ассемблере и работе операционных систем с памятью. Согласен, что называть такую разработку нормальной и полноценно обучающей, но по крайней мере мне интересно.

Буду рад любой конструктивной критике архитектуры, советам от старших коллег по оптимизации хелперов памяти и, конечно же, пулл‑реквестам!

Ссылка на репозиторий GitHub: Vladislav‑Kalinkin/wie

Спасибо за внимание! Жду вас в комментариях.