VB6Runtime / Documentation

Pluggable Backend Pattern for Process-Global Mutable State

Overview

The runtime state system in vb6runtime manages all process-global mutable state that spans individual VB6 function calls and statements. In real VB6, many operations rely on hidden, process-wide state that is not visible in the code itself: the environment table read by Environ$, the shared random number generator used by Rnd and Randomize, the current Err.Number, application settings persisted by SaveSetting and GetSetting, the system clock read by Date and Time statements, open file handles numbered 1–511, and user interaction hooks for MsgBox, Shell, SendKeys, and more.

The vb6runtime replicates all of this process-global state using a pluggable backend architecture that allows the same API to work across native platforms (Windows, Linux, macOS), WASM environments, and fully deterministic test harnesses. This document describes the architecture of the state system, its individual components, and how it is consumed by the library and its consumers.

Core principle: Every category of runtime state follows an identical structural pattern: a trait defining the interface, a native backend for real platform behavior, a memory backend for tests and WASM, and a public API module that delegates to whichever backend is active.

Categories of Managed State

The state system is organized into eight distinct categories, each managing a specific class of process-global mutable state:

Module Responsible VB6 Operations Backend Types
clock Date, Time, Date$, Time$ Native, Memory
environment Environ$(n) Native, Memory
file Open, Close, Get, Put, Print#, Input# Native, Memory
interaction Command$, DoEvents, Beep, MsgBox, InputBox, AppActivate, SendKeys, Shell Native, Memory
random Rnd, Randomize Classic, Modern, Playback
settings SaveSetting, GetSetting, GetAllSettings, DeleteSetting Registry, File, Memory
err Err.Number, omitted-argument Error / Error$ Atomic (no backend)
resources LoadResString, LoadResPicture, LoadResData Linked file (no backend)

Architecture

Every state category follows an identical structural pattern. Each category lives in its own module under state/<category> with the same file organization:

state/<category>/
mod.rs         → backend trait, singleton, public API
backend.rs     → (optional) separate trait file
memory.rs     → in-memory / test implementation
native.rs      → real OS implementation
[additional].rs → platform-specific or specialized backends

Each module exposes four key abstractions through its mod.rs:

  1. A singleton trait object stored as OnceLock<Mutex<Box<dyn XxxBackend>>> for lazy, thread-safe initialization.
  2. A default_backend() function that selects the appropriate backend for the current platform target.
  3. set_backend() and reset_backend() functions for runtime switching between backends.
  4. Public functions (e.g., open_file(), now()) that acquire the backend mutex and delegate to the active implementation.

The Backend Trait Pattern

Each state category defines a trait that abstracts all operations on that category of state. The trait defines the interface that every backend implementation must provide. Here is the pattern as used by the file I/O state:

trait FileBackend {
    fn open(
        &self,
        path: &Path,
        mode: OpenMode,
        access: AccessMode,
        lock: LockMode,
    ) -> Result<OpenFile, IoError>;

    fn close_file(&self, num: i16) -> Result<(), IoError>;
    fn read_file(&self, num: i16, buf: &mut [u8]) -> Result<usize, IoError>;
    fn write_file(&self, num: i16, buf: &[u8]) -> Result<usize, IoError>;
    fn seek_file(&self, num: i16, pos: FileSeek) -> Result<u64, IoError>;
    fn file_exists(&self, path: &Path) -> bool;
    fn delete_file(&self, path: &Path) -> Result<(), IoError>;
    // ... many more
}

The native.rs implementation delegates to real OS APIs (e.g., std::fs::File), while memory.rs stores all data in an in-memory HashMap<String, Vec<u8>> representing a virtual filesystem. The public API in mod.rs then looks like:

// Singleton for the active backend
static BACKEND: OnceLock<Mutex<Box<dyn FileBackend>>> = OnceLock::new();

// Thread-local open file handles (VB6 numbers them 1–511)
thread_local! {
    static OPEN_FILES: RefCell<HashMap<i16, OpenFile>> = RefCell::new(HashMap::new());
}

pub fn open_file(
    path: &Path,
    mode: OpenMode,
    access: AccessMode,
    lock: LockMode,
) -> Result<i16, IoError> {
    let backend = get_backend();
    let mut file = backend.open(path, mode, access, lock)?;
    let num = next_file_number();
    OPEN_FILES.with(|files| {
        files.borrow_mut().insert(num, file.clone());
    });
    Ok(num)
}

fn get_backend() -> Box<dyn FileBackend> {
    BACKEND.get().expect("File backend not initialized")
        .lock().expect("File backend poisoned").clone()
}

Default backend selection: On native targets, the NativeBackend is used. On WASM (target_arch = "wasm32"), MemoryBackend is selected automatically, since real file system access is not available in browsers.

State Categories in Detail

File I/O State (state/file/)

VB6 file I/O operates on numbered handles (1–511) rather than OS-level file descriptors. The file state system manages open file handles, their modes, positions, and print columns. Each OpenFile struct tracks:

The file state also manages a ROOT override for relative path resolution, allowing tests and sandboxed environments to root all file operations to a virtual directory tree.

enum OpenMode { Input, Output, Append, Binary, Random }
enum AccessMode { Read, Write, ReadWrite }
enum LockMode { Shared, DenyRead, DenyWrite, DenyReadWrite }

struct OpenFile {
    num: i16,
    mode: OpenMode,
    access: AccessMode,
    position: u64,
    print_column: Option<u32>,
    file: FileStorage,
}

Clock State (state/clock/)

The clock state provides a two-layer architecture: a system clock backed by a pluggable backend, and a mock clock that adds a signed offset on top. This design allows both real-time accuracy and precise test control:

Mock Clock (signed offset for test control)
MOCK_OFFSET: OnceLock<Mutex<Span>>
System Clock (pluggable: Native or Memory)
BACKEND: OnceLock<Mutex<Box<dyn ClockBackend>>>

The ClockBackend trait defines two operations: now() to read the current time as a jiff::Timestamp, and set() to write a new time. The NativeBackend queries the real OS clock via jiff::Timestamp::now(), while the MemoryBackend stores a time that can be manually advanced.

trait ClockBackend: Send + Sync {
    fn now(&self) -> Timestamp;
    fn set(&self, timestamp: Timestamp);
}

// Public API
pub fn get() -> DateTime {
    let sys = system_get();
    let offset = mock_offset();
    sys.checked_add(offset).unwrap_or(sys)
}

pub fn set_date(year: u32, month: u32, day: u32) {
    // Shift mock clock to a specific civil date/time
}

pub fn set_time(hour: u8, min: u8, sec: u8) {
    // Shift mock clock to a specific time of day
}

Why two layers? The dual-layer design lets consumers (like vb6interpret) set an initial date/time for testing without needing to implement a full custom backend. They simply configure the mock offset, and all calls to get() return the mocked time.

Environment State (state/environment/)

The environment state manages the process environment snapshot for Environ$(n). Unlike most state categories, the environment uses a snapshot pattern: on first access, the backend is queried once to seed an ordered environment table (EnvState) that is then amended by subsequent calls to set_env():

trait EnvironmentBackend: Send + Sync {
    fn load(&self) -> Vec<(String, String)>;
}

// The mutable snapshot, loaded once from backend
static SNAPSHOT: OnceLock<Mutex<EnvState>> = OnceLock::new();

pub fn env_at(n: u32) -> Option<String> {
    // 1-based indexing, matching VB6's Environ$(n)
    let snapshot = get_snapshot();
    snapshot.entry_at(n - 1)
}

pub fn set_env(key: &str, value: &str) {
    let mut snapshot = get_snapshot();
    snapshot.set(key, value);
}

The EnvState struct maintains both an ordered Vec (for 1-based Environ$(n) access) and a case-insensitive HashMap (for key-based lookups).

Random State (state/random/)

The random state manages the VB6 random number generator. VB6's Rnd function uses a specific 24-bit linear congruential generator (LCG), and the state system provides three backends:

Backend Description Use Case
ClassicBackend Exact VB6 LCG: seed = (seed × 0x43FD43FD + 0x00C39EC3) mod 2²⁴ Default — bitwise-compatible with real VB6
ModernBackend rand::StdRng from the rand crate Better statistical properties for simulations
PlaybackBackend Cycles through a fixed list of pre-seeded values Deterministic testing

The RandomBackend trait defines four operations:

trait RandomBackend: Send + Sync {
    fn next(&self) -> f64;
    fn current(&self) -> i32;
    fn seed_from_rnd_argument(&mut self, arg: i32);
    fn randomize(&mut self);
}

The ClassicBackend uses an AtomicI32 for the seed, making it lock-free for the critical next() path. The Randomize statement is handled by a VB6-specific splice() method that adjusts the internal state to match real VB6's behavior when Randomize is called with a numeric argument.

Why is Rnd stateful? In VB6, Rnd is not a pure function — calling Rnd with a negative argument reseeds the generator, and subsequent calls return values from a shared sequence. This means all VB6 modules share a single random number stream, and the order of Rnd calls across the entire program matters.

Interaction State (state/interaction/)

The interaction state covers all user-facing operations: Command$, DoEvents, Beep, MsgBox, InputBox, AppActivate, SendKeys, and Shell. This is the most diverse category, with each operation potentially requiring a different OS API:

trait InteractionBackend: Send + Sync {
    fn command_args(&self) -> Vec<String>;
    fn do_events(&self) -> bool;
    fn beep(&self) -> bool;
    fn stop(&self) -> bool;
    fn msg_box(
        &self,
        prompt: &str,
        style: MsgBoxStyle,
        title: &str,
    ) -> MsgBoxResult;
    fn input_box(
        &self,
        prompt: &str,
        title: &str,
        default: &str,
    ) -> String;
    fn app_activate(
        &self,
        title: &str,
    ) -> bool;
    fn send_keys(
        &self,
        keys: &str,
        wait: bool,
    ) -> bool;
    fn shell(
        &self,
        command: &str,
        style: AppWinStyle,
    ) -> Option<u32>;
}

The NativeBackend uses platform-specific APIs: Win32 APIs on Windows (e.g., MessageBoxW, ShellExecuteEx, SendInput), osascript on macOS, and tools like zenity and xdotool on Linux. The MemoryBackend provides fully scripted deterministic behavior with FIFO response queues and request logs.

Settings State (state/settings/)

The settings state manages the in-memory settings cache for SaveSetting and GetSetting. Unlike the Windows registry, vb6runtime supports multiple backends for cross-platform compatibility:

Backend Storage Platform
RegistryBackend Windows Registry: HKEY_CURRENT_USER\Software\VB and VBA Program Settings\ Windows
FileBackend Directory tree: <root>/<appname>/<section>/<key> Linux, macOS
MemoryBackend HashMap in memory WASM, tests

The SettingsBackend trait provides a full CRUD API for (appname, section, key) triples:

trait SettingsBackend: Send + Sync {
    fn get(
        &self,
        appname: &str,
        section: &str,
        key: Option<&str>,
    ) -> SettingsResponse;
    fn set(
        &self,
        appname: &str,
        section: &str,
        key: Option<&str>,
        value: Option<&str>,
    ) -> Result<(), SettingError>;
    fn delete(
        &self,
        appname: &str,
        section: Option<&str>,
        key: Option<&str>,
    ) -> Result<(), SettingError>;
    fn load(&self) -> Snapshot;
}

The system keeps a Snapshot in memory — a HashMap with case-insensitive indexing — that is loaded once from the backend and then amended by SaveSetting and DeleteSetting calls without touching the persistent store on every access.

Error State (state/err.rs)

The error state is the simplest category. It stores the current Err.Number as a single AtomicI32, with no pluggable backend needed:

static CURRENT_NUMBER: AtomicI32 = AtomicI32::new(0);

pub fn current_number() -> i32 {
    CURRENT_NUMBER.load(Ordering::SeqCst)
}

pub fn set_number(n: i32) {
    CURRENT_NUMBER.store(n, Ordering::SeqCst);
}

pub fn clear() {
    CURRENT_NUMBER.store(0, Ordering::SeqCst);
}

This is accessed by the omitted-argument forms of Error and Error$, which convert the current error number back into its description string.

Resource State (state/resources.rs)

The resource state links a compiled .res file to the runtime. When a VB6 project includes a resource file, vb6runtime loads and lazily parses it on first access by LoadResString, LoadResPicture, or LoadResData:

struct Linked {
    path: PathBuf,
    parsed: OnceLock<Result<ResFile, Error>>,
}

static RES_FILE: OnceLock<Linked> = OnceLock::new();

pub fn set_file(path: PathBuf) {
    RES_FILE.get_or_init(|| Linked { path, parsed: OnceLock::new() });
}

pub fn with_file<T>(f: impl FnOnce(&ResFile) -> T) -> Result<T, Error> {
    let linked = RES_FILE.get().ok_or(Error::NoResourceFile)?;
    let res_file = linked.parsed.get_or_try_init(|| parse_res_file(&linked.path))?;
    f(&res_file?)
}

State Evolution

The runtime state system does not use a traditional state machine with explicit transitions. Instead, state changes are event-driven mutations — each public API function acquires the backend mutex, delegates to the active backend, and returns the result. The state evolves through sequential method calls.

State lifecycle:

  1. Initialization: On first access, the OnceLock lazy-initializes the backend singleton via get_or_init(). The default backend is chosen by platform detection.
  2. Mutation: Each public function acquires the backend mutex, delegates to the active backend, and returns the result. There are no explicit state transitions — the state evolves through method calls.
  3. Backend switching: set_backend(new_backend) replaces the boxed trait object, immediately affecting all subsequent calls. The snapshot (for environment and settings) is rebuilt from the new backend.
  4. Reset: reset_backend() restores the platform-appropriate default. reset() (where available) rebuilds the in-memory snapshot from the backend.

Thread safety is provided at multiple levels. All pluggable backends are Send + Sync and wrapped in Mutex<Box<dyn Trait>>. The error number uses AtomicI32 for lock-free access. The file I/O state uses thread_local! for the open file handle table, since VB6 file numbers are per-thread (or more precisely, per-cooperative-context).

Consumer Integration

Vb6Interpret Configuration

The vb6interpret project is the primary consumer of vb6runtime's state system. Before executing a VB6 module, the interpreter stages values into the state system:

// In vb6interpret's config.rs
impl InterpreterConfig {
    pub fn set_environment(&mut self, key: &str, value: &str) {
        self.staged_env.push((key.to_string(), value.to_string()));
    }

    pub fn set_setting(
        &mut self,
        appname: &str,
        section: &str,
        key: &str,
        value: &str,
    ) {
        self.staged_settings.push((
            appname.to_string(),
            section.to_string(),
            key.to_string(),
            value.to_string(),
        ));
    }

    pub fn set_file_backend(&mut self, backend: Box<dyn FileBackend>) {
        self.file_backend = Some(backend);
    }

    pub fn set_allow_system_time(&mut self, allow: bool) {
        self.mock_clock_enabled = !allow;
    }
}

At the start of every run_module(), the interpreter applies staged values into the shared state:

fn run_module(&self, module: &Module) -> Result<ModuleOutput, RuntimeError> {
    // 1. Write staged environment variables into the shared snapshot
    for (key, value) in &self.staged_env {
        env_state::set_env(key, value);
    }

    // 2. Apply staged settings
    for (app, section, key, value) in &self.staged_settings {
        settings_state::set(app, section, key, value);
    }

    // 3. Link resource file or clear it
    if let Some(ref path) = self.resource_file {
        resources_state::set_file(path.clone());
    } else {
        resources_state::clear();
    }

    // 4. Configure clock: reset to real time or set initial date/time for mock clock
    if self.mock_clock_enabled {
        if let Some(ref initial) = self.initial_datetime {
            clock::set_date(initial.year(), initial.month() as u32, initial.day());
            clock::set_time(initial.hour(), initial.minute(), initial.second());
        }
    }

    // 5. Execute the module
    self.interpreter.execute(module)
}

Library Function Integration

The library/ modules implement VB6 standard library functions that read and write state. Each function is a thin wrapper that delegates to the appropriate state module:

// library/statements/date.rs
pub fn set_date(year: u32, month: u32, day: u32) -> Result<(), Error> {
    clock::set_date(year, month, day);
    Ok(())
}

// library/statements/savesetting.rs
pub fn save_setting(
    app_name: &str,
    section: &str,
    key: &str,
    value: &str,
) -> Result<(), Error> {
    settings_state::set(app_name, section, Some(key), Some(value))?;
    Ok(())
}

// library/random.rs
pub fn rnd(arg: Option<i32>) -> f64 {
    let mut backend = random_state::get_backend();
    if let Some(arg) = arg {
        backend.seed_from_rnd_argument(arg);
        random_state::set_backend(Box::new(backend));
        return 0.0;
    }
    random_state::next()
}

WASM Sandbox Mode

When compiled for the WASM target (target_arch = "wasm32"), vb6runtime operates in a fully sandboxed mode. The WASM bridge (run_bridge.rs) configures both the file and clock backends to MemoryBackend instances:

JavaScript Host
In-memory file system, clock sync
WASM Runtime (vb6runtime)
MemoryBackend (file + clock)

This enables:

Design Patterns

The runtime state system employs several recurring design patterns:

Pattern How It Is Used
Strategy Every state category has a trait with multiple implementations (Native, Memory, Registry, Playback)
Singleton + Lazy Init OnceLock<Mutex<Box<dyn Trait>>> for global backend access
Snapshot Environment and settings keep a mutable in-memory snapshot loaded from the backend, amended by calls
Facade Public module functions (e.g., file::open_file()) provide a clean API over the backend abstraction
Platform Polymorphism default_backend() uses platform detection to select the appropriate backend
Dependency Injection set_backend() allows swapping in deterministic backends for testing
Thread Safety Mutex for trait objects, AtomicI32 for error number, thread_local! for file handles
Dual-Layer Clock System clock (pluggable) + Mock clock (offset) gives both real-time accuracy and test control

Future Considerations

1. Fine-Grained Concurrency

Currently, all state categories use a single Mutex per backend, which serializes all access. For multi-threaded VB6 scenarios (using runtimes.dll apartment threading), finer-grained locking could improve throughput.

Trade-off: More complexity in the state management code, but better performance for multi-module concurrent execution.

2. State Checkpointing

For debugging and test replay, the ability to snapshot and restore the entire runtime state would be valuable:

  • Save current state to a serializable format
  • Restore from a checkpoint
  • Diff two states for debugging

Benefit: Enable "time travel" debugging and robust test replay.

3. Additional State Categories

Several VB6 stateful operations are not yet fully modeled:

  • Current directory (ChDrive, ChDir)
  • Default file mode (Mode statement)
  • Trace output destination (OutputTo)

Priority: Lower — these are used less frequently in typical VB6 applications.

Summary

The runtime state system provides a clean, extensible foundation for managing all process-global mutable state in the VB6 runtime. Its key design decisions — the pluggable backend trait pattern, platform-aware defaults, snapshot caching, and dual-layer clock — work together to satisfy the diverse requirements of real VB6 behavior, cross-platform compatibility, test determinism, and WASM sandboxing.

The system was designed because:

  1. VB6 has hidden state: Many VB6 operations depend on process-wide mutable state that is not visible in the source code.
  2. One size does not fit all: Real OS behavior, in-memory testing, and WASM sandboxing require fundamentally different implementations of the same operations.
  3. Testability by design: Deterministic backends (Memory, Playback) allow full control over runtime state for reliable testing.
  4. Single API, multiple backends: Library functions and consumers use the same public API regardless of which backend is active.
  5. Cross-platform from the start: The backend abstraction was designed for Windows, Linux, macOS, and WASM from the beginning, not as an afterthought.

The result: A unified runtime state system that powers accurate VB6 emulation across every target platform while remaining fully controllable for testing and debugging.