Project Write-up

WIE: A Technical Overview

WIE is an emulator for legacy Korean mobile application platforms โ€” primarily WIPI, with additional support for SKVM and J2ME. It runs entirely in the browser through Rust compiled to WebAssembly. This page is a walkthrough of how the project is organized internally, the trade-offs behind the architecture, and the historical context that makes the project necessary in the first place.

What WIPI Was

WIPI โ€” short for Wireless Internet Platform for Interoperability โ€” was the mobile application platform mandated on Korean feature phones from April 2005 to April 2009. During that period every domestically sold handset had to ship with a WIPI runtime, and most Korean mobile apps and games of that era were built on top of it.

In practice the platform was more of a family than a single specification. Each carrier shipped its own WIPI implementation, with carrier-specific extensions and binary formats. KTF compiled Java methods ahead-of-time into ARM native code packaged in a single client.bin file. LG Telecom used ELF native binaries with import-table integration into the runtime. SK Telecom's WIPI ran entirely inside its Java virtual machine, with no ARM execution involved. After the mandate was lifted, smartphones replaced feature phones within a few years and the entire ecosystem effectively wound down โ€” the hardware aged out of service, the carrier-side infrastructure was shut down, and the original applications became unrunnable in any practical sense.

Why an Emulator?

The motivation behind WIE is digital preservation and educational research rather than nostalgia for its own sake. A decade-plus of Korean mobile software history โ€” including games that were widely popular at the time but were never ported anywhere else โ€” exists today only as binary archives that no current device can execute. Without a working emulator, those binaries are functionally opaque: you can read them with a hex editor, but you can't run them, debug them, or study how they behave at runtime.

An emulator changes that. With WIE, a WIPI archive can be loaded into a browser and executed the same way the original handset would have executed it, with the runtime quirks and platform behaviors preserved. The same infrastructure also makes the binaries inspectable in ways the original devices never allowed: you can attach debug tracing to native calls, dump intermediate state, and cross-reference the running app against its reverse-engineered structure definitions.

Architecture at a Glance

Internally, WIE is organized around three layers. The host layer owns everything that touches the outside world: the window, audio output, filesystem, clock, and standard I/O. It's abstracted behind a single Platform trait so the same emulator core can be driven by a command-line host during development and by the web frontend in production.

The runtime layer sits above the host layer. It owns task scheduling, event delivery, audio playback, and the platform-neutral services that emulated applications expect to find. This is where the cooperative async executor lives, where the virtual filesystem is populated from the input archive, and where audio handles are managed.

Finally, the execution layer is where platform-specific application code actually runs. It's split between two engines: a Java virtual machine for bytecode execution, and an ARM emulator for the native code that some WIPI variants compile their applications into. Different platform crates (KTF, LGT, SKT, J2ME) choose which of these engines to use, and how to wire them together.

Platform Variants

WIE supports four distinct platform targets, each implemented as its own Rust crate sitting on top of the shared backend.

  • KTF โ€” the most demanding of the four. KTF apps ship as a JAR containing a client.bin file, which holds AOT-compiled ARM machine code for every Java method along with platform-specific class metadata. There are no .class files anywhere in the package. The emulator has to load the binary into ARM memory, interpret the metadata structures, and bridge the AOT-compiled code back to a Rust-hosted JVM so that Java semantics still work.
  • LGT โ€” uses native ELF binaries instead of an AOT-compiled blob. Integration with the runtime happens through an import table, which the emulator resolves against its Rust-side WIPI implementation. Like KTF, LGT runs as a mix of ARM execution and JVM execution.
  • SKT โ€” pure-JVM platform. No ARM core is created at all; the emulator builds a JVM with the required API surface and invokes the application's Java entry point directly.
  • J2ME โ€” the international Java ME standard, supported here for completeness. Also runs entirely inside the JVM, with MIDP APIs (LCDUI, RMS, media) provided by Rust-defined class prototypes.

The choice to keep platform crates separate, rather than forcing everything through a unified abstraction, is deliberate. Each platform has its own boot sequence, binary format, and native integration model. Trying to hide those differences behind a single API would either leak the differences anyway or impose constraints that don't match how the original runtimes actually worked.

Crossing the Rust/ARM Boundary

The most interesting part of the emulator โ€” and the part that took the most iteration to get right โ€” is how Rust code and emulated ARM code call each other. The basic mechanism is callback registration. Rust registers a function with the ARM core and receives a synthetic address in return. The ARM core embeds that address into its memory map; when ARM execution reaches it, the core suspends normal stepping and calls back into the registered Rust function. The Rust function can read and write emulated registers and memory, perform whatever host-side work is needed, and then hand control back to the ARM context.

This single pattern carries a surprising amount of weight. WIPI C functions exposed to the application are implemented this way. Java bridge trampolines that let AOT-compiled ARM code call into the Rust-hosted JVM use the same mechanism. Native import resolution, exception handling, allocation, and class loading all flow through registered callbacks. The result is that the ARM emulator only has to know how to execute ARM instructions and how to detect when it's hit a callback boundary; everything platform-specific lives on the Rust side, where it's easier to reason about and test.

Running in the Browser

Most of the crates in the WIE workspace are written as no_std crates. The host-facing crates can use std freely, but the emulator core, runtime services, and platform implementations are kept independent of the operating system. This is what makes the WebAssembly target practical: there's no accidental dependency on filesystem APIs, threading primitives, or anything else the browser sandbox doesn't provide.

The threading model is worth a closer look. ARM-backed platforms like KTF and LGT need thread-local ARM state, because the original handsets ran multiple ARM threads and apps depend on that behavior. The browser, however, doesn't expose real OS threads in a way that the emulator can use uniformly. WIE addresses this by representing thread-like behavior through a cooperative async executor: each ARM thread becomes an async task that owns its own ARM context, and the executor moves tasks in and out of the ARM core as needed. The same executor drives pure-JVM platforms directly, without any ARM involvement. The async runtime ends up being the portability layer that preserves thread semantics on a target that has no real threads.

Related Projects

WIE depends on several supporting libraries that are technically separate Cargo packages but are maintained alongside the emulator and exist primarily to serve it.

  • RustJava provides the JVM core itself โ€” class loading, runtime support, and the Rust-defined class prototype system that lets WIPI and MIDP APIs be implemented in Rust rather than Java.
  • wipi / wipi_types hold the reverse-engineered binary structure definitions and ABI-level constants used to decode pointers read from emulated memory. The same project also functions as a homebrew SDK for building new applications targeting the emulator.
  • smaf / smaf_player handle the Yamaha SMAF / MMF audio format used by most WIPI applications, parsing audio assets into timed events that get translated into PCM or MIDI output on the host side.

This page is a best-effort write-up โ€” some details may be inaccurate or out of date. If you spot an error, please flag it via the email on the home page.

Try it

WIE is open source under a permissive license. The browser build is hosted at the project site, the source and contribution guide are on GitHub, and the project continues to evolve as more platform-specific edge cases get pinned down.