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:
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:
- A singleton trait object stored as
OnceLock<Mutex<Box<dyn XxxBackend>>>for lazy, thread-safe initialization. - A
default_backend()function that selects the appropriate backend for the current platform target. set_backend()andreset_backend()functions for runtime switching between backends.- 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 underlying file handle or memory buffer
- The open mode (
Input,Output,Append,Binary,Random) - The access mode (
Read,Write,ReadWrite) - The lock mode (
Shared,DenyRead,DenyWrite,DenyReadWrite) - Current read/write position
- Print column position (for formatted output)
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:
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:
- Initialization: On first access, the
OnceLocklazy-initializes the backend singleton viaget_or_init(). The default backend is chosen by platform detection. - 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.
- 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. - 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:
In-memory file system, clock sync
MemoryBackend (file + clock)
This enables:
- File system sandboxing: All file operations target an in-memory
HashMapthat can be synced to/from the JS host. - Live-advancing clock: The
MemoryBackendis seeded with real time on initialization and advances in real time, providing natural behavior without OS clock access. - Full determinism: By controlling both file state and clock state, the entire execution environment becomes deterministic for testing and demo purposes.
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 (
Modestatement) - 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:
- VB6 has hidden state: Many VB6 operations depend on process-wide mutable state that is not visible in the source code.
- One size does not fit all: Real OS behavior, in-memory testing, and WASM sandboxing require fundamentally different implementations of the same operations.
- Testability by design: Deterministic backends (Memory, Playback) allow full control over runtime state for reliable testing.
- Single API, multiple backends: Library functions and consumers use the same public API regardless of which backend is active.
- 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.