pub mod gui;
use std::collections::BTreeMap;
use crate::emulator::emulator::mmu::Perm;
use crate::emulator::emulator::regs::{Reg8, Reg16};
use crate::emulator::emulator::{Cpu, Mmu};
use crate::emulator::win32::{HostState, Registry, Win32Error};
const WIN16_HEAP_BASE: u32 = 0x0040_0000;
const WIN16_HEAP_FIRST_SEL: u16 = 0x0200;
const WIN16_HEAP_WINDOW: u32 = 0x0001_0000;
#[derive(Debug, Default, Clone)]
pub struct Win16Heap {
next_base: u32,
next_selector: u16,
pub blocks: BTreeMap<u16, (u32, u32)>,
}
impl Win16Heap {
fn alloc(&mut self, cpu: &mut Cpu, mmu: &mut Mmu, size: u32) -> u16 {
if self.next_base < WIN16_HEAP_BASE {
self.next_base = WIN16_HEAP_BASE;
self.next_selector = WIN16_HEAP_FIRST_SEL;
}
let page_rounded = size.max(1).wrapping_add(0xFFF) & !0xFFF;
let mapped = page_rounded.max(WIN16_HEAP_WINDOW);
let base = self.next_base;
let sel = self.next_selector;
mmu.map(base, mapped, Perm::R | Perm::W | Perm::X);
cpu.define_selector(sel, base);
self.blocks.insert(sel, (base, size));
if std::env::var("UD_NE_HEAP_DEBUG").is_ok() {
eprintln!(
"win16_heap: alloc sel={sel:#06x} base={base:#010x}..{:#010x} \
(asked={size:#x}, mapped={mapped:#x})",
base.wrapping_add(mapped)
);
}
self.next_base = self.next_base.wrapping_add(mapped);
self.next_selector = self.next_selector.wrapping_add(8);
sel
}
fn realloc(&mut self, cpu: &mut Cpu, mmu: &mut Mmu, sel: u16, new_size: u32) -> u16 {
let Some(&(old_base, old_size)) = self.blocks.get(&sel) else {
return self.alloc(cpu, mmu, new_size);
};
if new_size <= WIN16_HEAP_WINDOW {
self.blocks.insert(sel, (old_base, new_size));
return sel;
}
let new_window = new_size.wrapping_add(0xFFF) & !0xFFF;
if self.next_base < WIN16_HEAP_BASE {
self.next_base = WIN16_HEAP_BASE;
self.next_selector = WIN16_HEAP_FIRST_SEL;
}
let new_base = self.next_base;
mmu.map(new_base, new_window, Perm::R | Perm::W | Perm::X);
for i in 0..old_size.min(new_size) {
let b = mmu.load8(old_base.wrapping_add(i)).unwrap_or(0);
let _ = mmu.store8(new_base.wrapping_add(i), b);
}
cpu.define_selector(sel, new_base); self.blocks.insert(sel, (new_base, new_size));
self.next_base = self.next_base.wrapping_add(new_window);
sel
}
}
pub const WIN16_HINSTANCE: u16 = 0x0100;
pub const WIN16_HTASK: u16 = 0x0200;
const SW_SHOWNORMAL: u16 = 1;
pub fn call_guest_far16(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &mut Registry,
state: &mut HostState,
proc_sel: u16,
proc_off: u16,
args: &[u16],
) -> Result<u32, crate::emulator::Error> {
use crate::emulator::emulator::isa_int::RET_SENTINEL;
cpu.define_selector(crate::emulator::ne::SENTINEL_SELECTOR, RET_SENTINEL);
for &a in args {
cpu.push16(mmu, a)?;
}
cpu.push16(mmu, crate::emulator::ne::SENTINEL_SELECTOR)?;
cpu.push16(mmu, 0)?;
cpu.set_cs_ip(proc_sel, proc_off);
crate::emulator::win32::run_until_sentinel(cpu, mmu, registry, state)?;
let ax = u32::from(cpu.regs.get16(Reg16::Ax));
let dx = u32::from(cpu.regs.get16(Reg16::Dx));
Ok((dx << 16) | ax)
}
pub fn register_all(registry: &mut Registry) {
register_kernel(registry);
register_user(registry);
register_gdi(registry);
register_commdlg(registry);
}
fn register_commdlg(registry: &mut Registry) {
registry.register_far_pascal("commdlg", "@27", stub_get_file_title, 10);
}
fn register_gdi(registry: &mut Registry) {
registry.register_far_pascal("gdi", "@80", stub_get_device_caps, 4);
registry.register_far_pascal("gdi", "@91", stub_get_text_extent, 8);
registry.register_far_pascal("gdi", "@66", stub_create_object, 4);
registry.register_far_pascal("gdi", "@61", stub_create_object, 8);
registry.register_far_pascal("gdi", "@69", stub_ret1_1word, 2);
registry.register_far_pascal("gdi", "@87", stub_create_object, 2);
registry.register_far_pascal("gdi", "@442", stub_create_object, 20);
registry.register_far_pascal("gdi", "@36", stub_create_object, 2);
registry.register_far_pascal("gdi", "@72", stub_create_object, 10);
}
fn stub_create_object(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(u32::from(state.gui.alloc_obj_handle()))
}
fn stub_get_text_extent(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let cb = cpu.stack_word(mmu, 4).unwrap_or(1); let width = 8u32.wrapping_mul(u32::from(cb)) & 0xFFFF;
Ok((16u32 << 16) | width)
}
fn stub_get_device_caps(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let index = cpu.stack_word(mmu, 4).unwrap_or(0);
let v: u16 = match index {
8 => 640, 10 => 480, 12 => 8, 14 => 1, 88 | 90 => 96, 24 => 20, _ => 0,
};
Ok(u32::from(v))
}
fn register_user(registry: &mut Registry) {
registry.register_far_pascal("user", "@5", stub_ret1_1word, 2);
registry.register_far_pascal("user", "@66", stub_ret_handle_1word, 2);
registry.register_far_pascal("user", "@68", stub_ret1_2word, 4);
registry.register_far_pascal("user", "@69", stub_ret_handle_1word, 2);
registry.register_far_pascal("user", "@179", stub_get_system_metrics, 2);
registry.register_far_pascal("user", "@180", stub_get_sys_color, 2);
registry.register_far_pascal("user", "@173", stub_create_object, 6);
registry.register_far_pascal("user", "@174", stub_create_object, 6);
registry.register_far_pascal("user", "@266", stub_ret1_1word, 2);
registry.register_far_pascal("user", "@176", stub_load_string, 10);
registry.register_far_pascal("user", "@291", stub_set_windows_hook_ex, 10);
registry.register_far_pascal("user", "@57", stub_register_class, 4);
registry.register_far_pascal("user", "@87", stub_dialog_box, 12);
registry.register_far_pascal("user", "@88", stub_end_dialog, 4);
registry.register_far_pascal("user", "@89", stub_create_dialog, 12);
registry.register_far_pascal("user", "@292", stub_ret1_2word, 4);
registry.register_far_pascal("user", "@1", stub_message_box, 12);
registry.register_far_pascal("user", "@229", stub_ret0_1word, 2);
registry.register_far_pascal("user", "@69", stub_create_object, 2);
registry.register_far_pascal("user", "@42", stub_ret1_2word, 4);
registry.register_far_pascal("user", "@262", stub_ret0_1word, 4);
registry.register_far_pascal("user", "@32", stub_get_window_rect, 6);
registry.register_far_pascal("user", "@232", stub_ret1_1word, 14);
registry.register_far_pascal("user", "@111", stub_send_message, 10);
registry.register_far_pascal("user", "@91", stub_get_dlg_item, 4);
registry.register_far_pascal("user", "@36", stub_get_window_text, 8);
registry.register_far_pascal("user", "@37", stub_set_window_text, 6);
registry.register_far_pascal("user", "@38", stub_get_window_text_length, 2);
registry.register_far_pascal("user", "@92", stub_set_dlg_item_text, 8);
registry.register_far_pascal("user", "@93", stub_get_dlg_item_text, 10);
registry.register_far_pascal("user", "@404", stub_get_class_info, 10);
registry.register_far_pascal("user", "@268", stub_global_add_atom, 4);
registry.register_far_pascal("user", "@269", stub_ret0_1word, 2);
}
fn stub_get_sys_color(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let index = cpu.stack_word(mmu, 4).unwrap_or(0);
let color: u32 = match index {
15 => 0x00C0_C0C0, 16 => 0x0080_8080, 20 => 0x00FF_FFFF, 18 | 8 => 0x0000_0000, 5 => 0x00FF_FFFF, _ => 0x0080_8080,
};
Ok(color)
}
fn stub_ret1_2word(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(1)
}
fn stub_ret_handle_1word(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0x0010)
}
fn stub_get_system_metrics(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let index = cpu.stack_word(mmu, 4).unwrap_or(0);
let v: u16 = match index {
0 => 640, 1 => 480, 2 => 16, 3 => 16, 4 => 19, 5 | 6 => 1, 11 => 32, 12 => 32, _ => 0,
};
Ok(u32::from(v))
}
fn stub_ret1_1word(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(1)
}
pub fn service_interrupt(num: u8, cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState) -> bool {
match num {
0x21 => {
dos_int21(cpu, mmu, state);
true
}
0x03 => true,
_ => false,
}
}
fn dos_int21(cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState) {
use crate::emulator::emulator::isa_int::Seg;
let ah = cpu.regs.get8(Reg8::Ah);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
eprintln!("DOS INT21 AH={ah:#04x}");
}
cpu.regs.flags.cf = false; match ah {
0x30 => {
cpu.regs.set8(Reg8::Al, 6); cpu.regs.set8(Reg8::Ah, 0);
cpu.regs.set16(Reg16::Bx, 0);
cpu.regs.set16(Reg16::Cx, 0);
}
0x19 => cpu.regs.set8(Reg8::Al, 2),
0x25 => {}
0x1A => {
state.dos_dta = (cpu.segment_selector(Seg::Ds), cpu.regs.get16(Reg16::Dx));
}
0x2F => {
cpu.set_segment_reg(0 , state.dos_dta.0);
cpu.regs.set16(Reg16::Bx, state.dos_dta.1);
}
0x4E => {
let lin = cpu
.seg_base(Seg::Ds)
.wrapping_add(u32::from(cpu.regs.get16(Reg16::Dx)));
let spec = String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 260)).into_owned();
let size = state
.context
.vfs
.as_ref()
.and_then(|v| dos_find_match(v, &spec));
match size {
Some((name, sz)) => {
let dta = cpu.far_to_linear(state.dos_dta.0, state.dos_dta.1);
let _ = mmu.store8(dta.wrapping_add(0x15), 0x20); for o in [0x16u32, 0x18] {
let _ = mmu.store16(dta.wrapping_add(o), 0);
}
let _ = mmu.store16(dta.wrapping_add(0x1A), sz as u16);
let _ = mmu.store16(dta.wrapping_add(0x1C), (sz >> 16) as u16);
write_guest_cstr(mmu, dta.wrapping_add(0x1E), 13, name.as_bytes()).ok();
}
None => dos_error(cpu, 18), }
}
0x4F => dos_error(cpu, 18),
0x35 => {
cpu.regs.set16(Reg16::Bx, 0);
cpu.set_segment_reg(0, 0);
}
0x2A | 0x2C => {
cpu.regs.set16(Reg16::Cx, 0);
cpu.regs.set16(Reg16::Dx, 0);
}
0x3C | 0x3D => dos_open_or_create(cpu, mmu, state, ah == 0x3C),
0x3E => dos_close(cpu, state),
0x3F => dos_read(cpu, mmu, state),
0x40 => dos_write(cpu, mmu, state),
0x42 => dos_seek(cpu, state),
0x43 => dos_file_attribs(cpu, mmu, state),
0x44 => dos_ioctl(cpu, state),
0x39 | 0x3B | 0x3A => {}
_ => cpu.regs.set16(Reg16::Ax, 0),
}
}
fn dos_ds_dx_linear(cpu: &Cpu) -> u32 {
let ds = cpu.segment_selector(crate::emulator::emulator::isa_int::Seg::Ds);
let dx = cpu.regs.get16(Reg16::Dx);
cpu.far_to_linear(ds, dx)
}
fn read_dos_path(mmu: &Mmu, addr: u32) -> String {
String::from_utf8_lossy(&read_guest_cstr(mmu, addr, 260)).into_owned()
}
fn dos_error(cpu: &mut Cpu, code: u16) {
cpu.regs.flags.cf = true;
cpu.regs.set16(Reg16::Ax, code);
}
fn dos_open_or_create(cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState, create: bool) {
use crate::emulator::context::FileAccess;
let path_lin = dos_ds_dx_linear(cpu);
let path = read_dos_path(mmu, path_lin);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
let ds = cpu.segment_selector(crate::emulator::emulator::isa_int::Seg::Ds);
let dx = cpu.regs.get16(Reg16::Dx);
let al = cpu.regs.get8(Reg8::Al);
eprintln!(
" DOS {} DS:DX={ds:#06x}:{dx:#06x} lin={path_lin:#x} AL={al:#04x} path={path:?}",
if create { "create" } else { "open" }
);
}
if path.is_empty() {
dos_error(cpu, 0x02); return;
}
let access = if create {
FileAccess::ReadWrite
} else {
match cpu.regs.get8(Reg8::Al) & 0x07 {
0 => FileAccess::Read,
1 => FileAccess::Write,
2 => FileAccess::ReadWrite,
_ => FileAccess::Read,
}
};
let vfs = state
.context
.vfs
.get_or_insert_with(crate::emulator::fsmount::MountTable::new);
if create {
vfs.write_path(&path, Vec::new());
}
let Some(vh) = vfs.open(&path, access) else {
dos_error(cpu, 0x02); return;
};
let dh = state.next_dos_handle;
state.next_dos_handle = state.next_dos_handle.wrapping_add(1);
state.dos_handles.insert(dh, vh);
cpu.regs.set16(Reg16::Ax, dh);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
eprintln!(
"DOS {} '{path}' → dh={dh:#06x} vh={vh:#x}",
if create { "create" } else { "open" }
);
}
}
fn dos_close(cpu: &mut Cpu, state: &mut HostState) {
let dh = cpu.regs.get16(Reg16::Bx);
if let Some(vh) = state.dos_handles.remove(&dh) {
if let Some(vfs) = state.context.vfs.as_mut() {
vfs.close(vh);
}
cpu.regs.set16(Reg16::Ax, 0);
} else {
dos_error(cpu, 0x06); }
}
fn dos_read(cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState) {
let dh = cpu.regs.get16(Reg16::Bx);
let count = usize::from(cpu.regs.get16(Reg16::Cx));
let buf_lin = dos_ds_dx_linear(cpu);
let Some(&vh) = state.dos_handles.get(&dh) else {
dos_error(cpu, 0x06);
return;
};
let Some(vfs) = state.context.vfs.as_mut() else {
dos_error(cpu, 0x06);
return;
};
let mut tmp = vec![0u8; count];
let n = vfs.read_handle(vh, &mut tmp).unwrap_or(0);
for (i, &b) in tmp[..n].iter().enumerate() {
let _ = mmu.store8(buf_lin.wrapping_add(i as u32), b);
}
cpu.regs.set16(Reg16::Ax, n as u16);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
eprintln!(" DOS read dh={dh:#06x} count={count} -> n={n}");
}
}
fn dos_write(cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState) {
let dh = cpu.regs.get16(Reg16::Bx);
let count = usize::from(cpu.regs.get16(Reg16::Cx));
let buf_lin = dos_ds_dx_linear(cpu);
let Some(&vh) = state.dos_handles.get(&dh) else {
dos_error(cpu, 0x06);
return;
};
let mut data = vec![0u8; count];
for (i, b) in data.iter_mut().enumerate() {
*b = mmu.load8(buf_lin.wrapping_add(i as u32)).unwrap_or(0);
}
let Some(vfs) = state.context.vfs.as_mut() else {
dos_error(cpu, 0x06);
return;
};
let n = vfs.write_handle(vh, &data).unwrap_or(0);
cpu.regs.set16(Reg16::Ax, n as u16);
}
fn dos_file_attribs(cpu: &mut Cpu, mmu: &mut Mmu, state: &mut HostState) {
let al = cpu.regs.get8(Reg8::Al);
let path_lin = dos_ds_dx_linear(cpu);
let path = read_dos_path(mmu, path_lin);
if path.is_empty() {
dos_error(cpu, 0x02);
return;
}
let vfs = state
.context
.vfs
.get_or_insert_with(crate::emulator::fsmount::MountTable::new);
if !vfs.contains(&path) {
dos_error(cpu, 0x02);
return;
}
if al == 0 {
cpu.regs.set16(Reg16::Cx, 0x0020);
cpu.regs.set16(Reg16::Ax, 0x0020);
} else {
cpu.regs.set16(Reg16::Ax, 0);
}
}
fn dos_ioctl(cpu: &mut Cpu, state: &mut HostState) {
let al = cpu.regs.get8(Reg8::Al);
if al != 0x00 {
cpu.regs.set16(Reg16::Ax, 0);
return;
}
let dh = cpu.regs.get16(Reg16::Bx);
if !state.dos_handles.contains_key(&dh) {
dos_error(cpu, 0x06);
return;
}
cpu.regs.set16(Reg16::Dx, 0x0002);
cpu.regs.set16(Reg16::Ax, 0x0002);
}
fn dos_seek(cpu: &mut Cpu, state: &mut HostState) {
let dh = cpu.regs.get16(Reg16::Bx);
let origin = cpu.regs.get8(Reg8::Al);
let off = (u32::from(cpu.regs.get16(Reg16::Cx)) << 16) | u32::from(cpu.regs.get16(Reg16::Dx));
let signed_off = off as i32;
let Some(&vh) = state.dos_handles.get(&dh) else {
dos_error(cpu, 0x06);
return;
};
let Some(vfs) = state.context.vfs.as_mut() else {
dos_error(cpu, 0x06);
return;
};
if origin > 2 {
dos_error(cpu, 0x01); return;
}
let new_pos = vfs.seek_handle(vh, signed_off, origin).unwrap_or(0);
cpu.regs.set16(Reg16::Ax, (new_pos & 0xFFFF) as u16);
cpu.regs.set16(Reg16::Dx, ((new_pos >> 16) & 0xFFFF) as u16);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
eprintln!(" DOS seek dh={dh:#06x} origin={origin} off={signed_off} -> pos={new_pos}");
}
}
fn dos_find_match(vfs: &crate::emulator::fsmount::MountTable, spec: &str) -> Option<(String, u32)> {
let base = |p: &str| {
p.rsplit(['\\', '/'])
.next()
.unwrap_or(p)
.to_ascii_uppercase()
};
if !spec.contains('*') && !spec.contains('?') {
return vfs.read(spec).map(|b| (base(spec), b.len() as u32));
}
let norm = |s: &str| s.replace('\\', "/").to_ascii_lowercase();
let dir = norm(spec.rsplit_once(['\\', '/']).map_or("", |(d, _)| d));
for (path, len) in vfs.list() {
let p = norm(path);
let pdir = p.rsplit_once('/').map_or("", |(d, _)| d);
if pdir == dir.trim_start_matches(|c| c == 'c' || c == ':' || c == '/') {
return Some((base(path), len as u32));
}
}
None
}
fn register_kernel(registry: &mut Registry) {
registry.register_far_pascal("kernel", "@91", stub_init_task, 0);
registry.register_far_pascal("kernel", "@23", stub_lock_segment, 2);
registry.register_far_pascal("kernel", "@24", stub_ret0_1word, 2);
registry.register_far_pascal("kernel", "@30", stub_ret0_1word, 2);
registry.register_far_pascal("kernel", "@3", stub_get_version, 0);
registry.register_far_pascal("kernel", "@15", stub_global_alloc, 6);
registry.register_far_pascal("kernel", "@16", stub_global_realloc, 8);
registry.register_far_pascal("kernel", "@17", stub_ret0_1word, 2);
registry.register_far_pascal("kernel", "@18", stub_global_lock, 2);
registry.register_far_pascal("kernel", "@19", stub_ret0_1word, 2);
registry.register_far_pascal("kernel", "@20", stub_global_size, 2);
registry.register_far_pascal("kernel", "@49", stub_get_module_filename, 8);
registry.register_far_pascal("kernel", "@51", stub_make_proc_instance, 6);
registry.register_far_pascal("kernel", "@52", stub_ret1_2word, 4);
registry.register_far_pascal("kernel", "@134", stub_get_windows_directory, 6);
registry.register_far_pascal("kernel", "@135", stub_get_system_directory, 6);
registry.register_far_pascal("kernel", "@131", stub_echo_1word, 2);
registry.register_far_pascal("kernel", "@89", stub_lstrcat, 8);
registry.register_far_pascal("kernel", "@90", stub_lstrlen, 4);
registry.register_far_pascal("kernel", "@137", stub_fatal_app_exit, 6);
registry.register_far_pascal("kernel", "@47", stub_get_module_handle, 4);
registry.register_far_pascal("kernel", "@36", stub_get_current_task, 0);
registry.register_far_pascal("kernel", "@55", stub_catch, 4);
registry.register_far_pascal("kernel", "@56", stub_throw, 6);
registry.register_far_pascal("kernel", "@107", stub_ret0_1word, 2);
registry.register_far_pascal("kernel", "@132", stub_get_win_flags, 0);
registry.register_far_pascal("kernel", "@60", stub_find_resource, 10);
registry.register_far_pascal("kernel", "@61", stub_load_resource, 4);
registry.register_far_pascal("kernel", "@62", stub_lock_resource, 2);
registry.register_far_pascal("kernel", "@63", stub_free_resource, 2);
registry.register_far_pascal("kernel", "@65", stub_sizeof_resource, 4);
registry.register_far_pascal("kernel", "@134", stub_get_windows_dir, 6);
registry.register_far_pascal("kernel", "@135", stub_get_system_dir, 6);
}
fn stub_get_windows_dir(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf = far_arg_linear(cpu, mmu, 6);
write_guest_cstr(mmu, buf, n, b"C:\\WINDOWS")
}
fn stub_get_system_dir(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf = far_arg_linear(cpu, mmu, 6);
write_guest_cstr(mmu, buf, n, b"C:\\WINDOWS\\SYSTEM")
}
fn stub_echo_1word(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(u32::from(cpu.stack_word(mmu, 4).unwrap_or(0)))
}
fn stub_make_proc_instance(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
cpu.stack_dword(mmu, 4)
.map_err(|t| crate::emulator::win32::trap_to_win32_local("MakeProcInstance", t))
}
fn stub_get_module_filename(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_size = cpu.stack_word(mmu, 4).unwrap_or(0);
let off = cpu.stack_word(mmu, 6).unwrap_or(0);
let sel = cpu.stack_word(mmu, 8).unwrap_or(0);
let lin = cpu.far_to_linear(sel, off);
write_guest_cstr(mmu, lin, n_size, b"C:\\SITEX10.EXE")
}
fn stub_get_windows_directory(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_size = cpu.stack_word(mmu, 4).unwrap_or(0);
let off = cpu.stack_word(mmu, 6).unwrap_or(0);
let sel = cpu.stack_word(mmu, 8).unwrap_or(0);
let lin = cpu.far_to_linear(sel, off);
write_guest_cstr(mmu, lin, n_size, b"C:\\WINDOWS")
}
fn stub_get_system_directory(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_size = cpu.stack_word(mmu, 4).unwrap_or(0);
let off = cpu.stack_word(mmu, 6).unwrap_or(0);
let sel = cpu.stack_word(mmu, 8).unwrap_or(0);
let lin = cpu.far_to_linear(sel, off);
write_guest_cstr(mmu, lin, n_size, b"C:\\WINDOWS\\SYSTEM")
}
fn write_guest_cstr(mmu: &mut Mmu, addr: u32, max: u16, text: &[u8]) -> Result<u32, Win32Error> {
if max == 0 {
return Ok(0);
}
let limit = usize::from(max).saturating_sub(1).min(text.len());
for (i, &b) in text.iter().take(limit).enumerate() {
let _ = mmu.store8(addr.wrapping_add(i as u32), b);
}
let _ = mmu.store8(addr.wrapping_add(limit as u32), 0);
Ok(limit as u32)
}
fn read_guest_cstr(mmu: &Mmu, addr: u32, max: usize) -> Vec<u8> {
let mut out = Vec::new();
for i in 0..max as u32 {
match mmu.load8(addr.wrapping_add(i)) {
Ok(0) | Err(_) => break,
Ok(b) => out.push(b),
}
}
out
}
fn far_arg_linear(cpu: &Cpu, mmu: &Mmu, byte_off: u32) -> u32 {
let v = cpu.stack_dword(mmu, byte_off).unwrap_or(0);
cpu.far_to_linear((v >> 16) as u16, v as u16)
}
fn stub_fatal_app_exit(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let lin = far_arg_linear(cpu, mmu, 4);
let msg = if lin == 0 {
String::new()
} else {
String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 1024)).into_owned()
};
if msg.is_empty() {
state.message_box_log.push("FatalAppExit".into());
} else {
state.message_box_log.push(format!("FatalAppExit: {msg}"));
}
state.exit_requested = Some(0);
state.cur_process_mut().exit_code = Some(0);
Ok(0)
}
fn stub_lstrlen(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let lin = far_arg_linear(cpu, mmu, 4);
Ok(read_guest_cstr(mmu, lin, 0x8000).len() as u32)
}
fn stub_lstrcat(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let dst_ptr = cpu.stack_dword(mmu, 8).unwrap_or(0);
let dst_lin = cpu.far_to_linear((dst_ptr >> 16) as u16, dst_ptr as u16);
let src_lin = far_arg_linear(cpu, mmu, 4);
let src = read_guest_cstr(mmu, src_lin, 0x4000);
let mut end = dst_lin;
while !matches!(mmu.load8(end), Ok(0) | Err(_)) {
end = end.wrapping_add(1);
}
for (i, &b) in src.iter().enumerate() {
let _ = mmu.store8(end.wrapping_add(i as u32), b);
}
let _ = mmu.store8(end.wrapping_add(src.len() as u32), 0);
Ok(dst_ptr)
}
fn stub_def_window_proc(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0)
}
fn default_wndproc_farptr(registry: &mut Registry) -> (u16, u16) {
let thunk = registry.register_far_pascal("user", "@_defwndproc", stub_def_window_proc, 10);
(
crate::emulator::ne::IMPORT_SELECTOR,
(thunk - crate::emulator::win32::THUNK_BASE) as u16,
)
}
fn stub_get_class_info(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
registry: &mut Registry,
) -> Result<u32, Win32Error> {
let out = far_arg_linear(cpu, mmu, 4);
let (sel, off) = default_wndproc_farptr(registry);
let _ = mmu.load16(out); let _ = mmu.store16(out, 0); let _ = mmu.store16(out.wrapping_add(2), off);
let _ = mmu.store16(out.wrapping_add(4), sel);
Ok(1)
}
fn stub_register_class(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let wc = far_arg_linear(cpu, mmu, 4);
let style = u32::from(mmu.load16(wc).unwrap_or(0));
let wndproc_off = mmu.load16(wc.wrapping_add(2)).unwrap_or(0);
let wndproc_sel = mmu.load16(wc.wrapping_add(4)).unwrap_or(0);
let name_off = mmu.load16(wc.wrapping_add(22)).unwrap_or(0);
let name_sel = mmu.load16(wc.wrapping_add(24)).unwrap_or(0);
let name_lin = cpu.far_to_linear(name_sel, name_off);
let name = String::from_utf8_lossy(&read_guest_cstr(mmu, name_lin, 256)).into_owned();
state
.gui
.register_class(&name, wndproc_sel, wndproc_off, style);
Ok(u32::from(state.gui.alloc_obj_handle()))
}
fn stub_set_windows_hook_ex(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(u32::from(state.gui.alloc_obj_handle()))
}
#[derive(Debug, Clone)]
pub struct LoadedResource {
pub type_id: crate::format::ne::ResId,
pub name_id: crate::format::ne::ResId,
pub data: Vec<u8>,
}
fn res_id_arg(cpu: &Cpu, mmu: &Mmu, byte_off: u32) -> crate::format::ne::ResId {
use crate::format::ne::ResId;
let v = cpu.stack_dword(mmu, byte_off).unwrap_or(0);
let selector = (v >> 16) as u16;
if selector == 0 {
ResId::Int(v as u16)
} else {
let lin = cpu.far_to_linear(selector, v as u16);
ResId::Name(String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 256)).into_owned())
}
}
fn res_id_eq(a: &crate::format::ne::ResId, b: &crate::format::ne::ResId) -> bool {
use crate::format::ne::ResId;
match (a, b) {
(ResId::Int(x), ResId::Int(y)) => x == y,
(ResId::Name(x), ResId::Name(y)) => x.eq_ignore_ascii_case(y),
_ => false,
}
}
fn stub_find_resource(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let want_type = res_id_arg(cpu, mmu, 4);
let want_name = res_id_arg(cpu, mmu, 8);
let hrsrc = state
.resources
.iter()
.position(|r| res_id_eq(&r.type_id, &want_type) && res_id_eq(&r.name_id, &want_name))
.map_or(0, |i| i as u32 + 1);
if std::env::var("UD_NE_STUB_DEBUG").is_ok() {
eprintln!("FindResource type={want_type:?} name={want_name:?} -> {hrsrc}");
}
Ok(hrsrc)
}
fn stub_load_resource(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let hrsrc = cpu.stack_word(mmu, 4).unwrap_or(0);
let Some(res) = state.resources.get(hrsrc.wrapping_sub(1) as usize) else {
return Ok(0);
};
let data = res.data.clone();
let sel = state.win16_heap.alloc(cpu, mmu, data.len() as u32);
let base = cpu.far_to_linear(sel, 0);
for (i, &b) in data.iter().enumerate() {
let _ = mmu.store8(base.wrapping_add(i as u32), b);
}
Ok(u32::from(sel))
}
fn stub_lock_resource(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let sel = _cpu.stack_word(_mmu, 4).unwrap_or(0);
Ok(u32::from(sel) << 16)
}
fn stub_sizeof_resource(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let hrsrc = cpu.stack_word(mmu, 4).unwrap_or(0);
let size = state
.resources
.get(hrsrc.wrapping_sub(1) as usize)
.map_or(0, |r| r.data.len() as u32);
Ok(size)
}
fn stub_free_resource(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0)
}
fn stub_catch(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
use crate::emulator::emulator::isa_int::Seg;
let ret_off = cpu.stack_word(mmu, 0).unwrap_or(0);
let ret_seg = cpu.stack_word(mmu, 2).unwrap_or(0);
let buf_off = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf_seg = cpu.stack_word(mmu, 6).unwrap_or(0);
let buf = cpu.far_to_linear(buf_seg, buf_off);
let sp_after = (cpu.regs.get16(Reg16::Sp)).wrapping_add(8);
let w = |mmu: &mut Mmu, off: u32, v: u16| {
let _ = mmu.store16(buf.wrapping_add(off), v);
};
w(mmu, 0, sp_after);
w(mmu, 2, cpu.segment_selector(Seg::Ss));
w(mmu, 4, cpu.regs.get16(Reg16::Bp));
w(mmu, 6, cpu.regs.get16(Reg16::Si));
w(mmu, 8, cpu.regs.get16(Reg16::Di));
w(mmu, 10, cpu.segment_selector(Seg::Ds));
w(mmu, 12, cpu.segment_selector(Seg::Es));
w(mmu, 14, ret_off);
w(mmu, 16, ret_seg);
Ok(0)
}
fn stub_throw(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
use crate::emulator::emulator::isa_int::Seg;
let throwback = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf_off = cpu.stack_word(mmu, 6).unwrap_or(0);
let buf_seg = cpu.stack_word(mmu, 8).unwrap_or(0);
let buf = cpu.far_to_linear(buf_seg, buf_off);
let r = |mmu: &Mmu, off: u32| mmu.load16(buf.wrapping_add(off)).unwrap_or(0);
let saved_sp = r(mmu, 0);
let saved_ss = r(mmu, 2);
let saved_bp = r(mmu, 4);
let saved_si = r(mmu, 6);
let saved_di = r(mmu, 8);
let saved_ds = r(mmu, 10);
let saved_es = r(mmu, 12);
let ret_off = r(mmu, 14);
let ret_seg = r(mmu, 16);
cpu.regs.set16(Reg16::Bp, saved_bp);
cpu.regs.set16(Reg16::Si, saved_si);
cpu.regs.set16(Reg16::Di, saved_di);
cpu.load_segment(Seg::Ds, saved_ds);
cpu.load_segment(Seg::Es, saved_es);
let sp_new = saved_sp.wrapping_sub(10);
cpu.set_ss_sp(saved_ss, sp_new);
let ss_base = cpu.seg_base(Seg::Ss);
let _ = mmu.store16(ss_base.wrapping_add(u32::from(sp_new)), ret_off);
let _ = mmu.store16(
ss_base.wrapping_add(u32::from(sp_new)).wrapping_add(2),
ret_seg,
);
Ok(u32::from(throwback))
}
fn stub_get_window_rect(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let r = far_arg_linear(cpu, mmu, 4);
for (i, v) in [0u16, 0, 640, 480].iter().enumerate() {
let _ = mmu.store16(r.wrapping_add(i as u32 * 2), *v);
}
Ok(1)
}
fn stub_send_message(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let hwnd = cpu.stack_word(mmu, 12).unwrap_or(0);
let msg = cpu.stack_word(mmu, 10).unwrap_or(0);
let wparam = cpu.stack_word(mmu, 8).unwrap_or(0);
let lparam = cpu.stack_dword(mmu, 4).unwrap_or(0);
let lp_lin = || cpu.far_to_linear((lparam >> 16) as u16, lparam as u16);
match msg {
0x000C => {
let text = String::from_utf8_lossy(&read_guest_cstr(mmu, lp_lin(), 1024)).into_owned();
if let Some(w) = state.gui.windows.get_mut(&hwnd) {
w.title = text;
}
Ok(1)
}
0x000D => {
let text = state
.gui
.windows
.get(&hwnd)
.map(|w| w.title.clone())
.unwrap_or_default();
write_guest_cstr(mmu, lp_lin(), wparam, text.as_bytes())
}
0x000E => {
Ok(state
.gui
.windows
.get(&hwnd)
.map_or(0, |w| w.title.len() as u32))
}
0x00F0 => Ok(u32::from(
state.dialog_checks.get(&hwnd).copied().unwrap_or(0),
)), 0x00F1 => {
state.dialog_checks.insert(hwnd, wparam);
Ok(0)
}
_ => Ok(0),
}
}
fn stub_get_dlg_item(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let id = cpu.stack_word(mmu, 4).unwrap_or(0);
Ok(u32::from(state.dialog_items.get(&id).copied().unwrap_or(0)))
}
fn stub_get_dlg_item_text(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_max = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf = far_arg_linear(cpu, mmu, 6);
let id = cpu.stack_word(mmu, 10).unwrap_or(0);
let text = state
.dialog_items
.get(&id)
.and_then(|h| state.gui.windows.get(h))
.map(|w| w.title.clone())
.unwrap_or_default();
if n_max == 0 {
return Ok(0);
}
write_guest_cstr(mmu, buf, n_max, text.as_bytes())
}
fn stub_set_dlg_item_text(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let lin = far_arg_linear(cpu, mmu, 4);
let id = cpu.stack_word(mmu, 6).unwrap_or(0);
let text = String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 1024)).into_owned();
if let Some(&hwnd) = state.dialog_items.get(&id) {
if let Some(w) = state.gui.windows.get_mut(&hwnd) {
w.title = text.clone();
}
state
.gui
.events
.push(gui::GuiEvent::SetWindowText { hwnd, text });
}
Ok(1)
}
fn stub_get_window_text_length(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let hwnd = cpu.stack_word(mmu, 4).unwrap_or(0);
Ok(state
.gui
.windows
.get(&hwnd)
.map_or(0, |w| w.title.len() as u32))
}
fn stub_get_window_text(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_max = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf = far_arg_linear(cpu, mmu, 6);
let hwnd = cpu.stack_word(mmu, 10).unwrap_or(0);
let text = state
.gui
.windows
.get(&hwnd)
.map(|w| w.title.clone())
.unwrap_or_default();
if n_max == 0 {
return Ok(0);
}
write_guest_cstr(mmu, buf, n_max, text.as_bytes())
}
fn stub_set_window_text(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let lin = far_arg_linear(cpu, mmu, 4);
let hwnd = cpu.stack_word(mmu, 8).unwrap_or(0);
let text = String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 1024)).into_owned();
if let Some(w) = state.gui.windows.get_mut(&hwnd) {
w.title = text.clone();
}
state
.gui
.events
.push(gui::GuiEvent::SetWindowText { hwnd, text });
Ok(1)
}
fn stub_message_box(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let wtype = cpu.stack_word(mmu, 4).unwrap_or(0);
let cap_lin = far_arg_linear(cpu, mmu, 6);
let text_lin = far_arg_linear(cpu, mmu, 10);
let caption = String::from_utf8_lossy(&read_guest_cstr(mmu, cap_lin, 512)).into_owned();
let text = String::from_utf8_lossy(&read_guest_cstr(mmu, text_lin, 2048)).into_owned();
let result: u16 = match wtype & 0x000F {
3 | 4 => 6, 5 => 4, _ => 1, };
state.message_box_log.push(format!("[{caption}] {text}"));
state.gui.events.push(gui::GuiEvent::MessageBox {
caption,
text,
flags: wtype,
result,
});
Ok(u32::from(result))
}
const WM_INITDIALOG: u16 = 0x0110;
const WM_COMMAND: u16 = 0x0111;
fn stub_create_dialog(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
registry: &mut Registry,
) -> Result<u32, Win32Error> {
let proc_off = cpu.stack_word(mmu, 4).unwrap_or(0);
let proc_sel = cpu.stack_word(mmu, 6).unwrap_or(0);
let tmpl_off = cpu.stack_word(mmu, 10).unwrap_or(0);
let tmpl_sel = cpu.stack_word(mmu, 12).unwrap_or(0);
let this_sel = cpu.segment_selector(crate::emulator::emulator::isa_int::Seg::Es);
let this_off = cpu.regs.get16(Reg16::Bx);
let want = if tmpl_sel == 0 {
crate::format::ne::ResId::Int(tmpl_off)
} else {
let lin = cpu.far_to_linear(tmpl_sel, tmpl_off);
crate::format::ne::ResId::Name(
String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 256)).into_owned(),
)
};
let dlg = state
.resources
.iter()
.find(|r| r.type_id == crate::format::ne::ResId::Int(5) && res_id_eq(&r.name_id, &want))
.map(|r| r.data.clone());
let (title, controls) = dlg
.as_deref()
.map_or_else(|| (String::new(), Vec::new()), gui::parse_dialog_template);
state.gui.events.push(gui::GuiEvent::DialogStart {
title,
controls: controls.clone(),
});
let hdlg = state.gui.alloc_hwnd();
for c in &controls {
let chwnd = state
.gui
.create_window(&c.class, &c.text, hdlg, c.id, c.style);
state.dialog_items.insert(c.id, chwnd);
}
let _ = (proc_sel, proc_off, this_sel, this_off, registry);
Ok(u32::from(hdlg))
}
fn stub_dialog_box(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
registry: &mut Registry,
) -> Result<u32, Win32Error> {
let proc_off = cpu.stack_word(mmu, 4).unwrap_or(0);
let proc_sel = cpu.stack_word(mmu, 6).unwrap_or(0);
let tmpl_off = cpu.stack_word(mmu, 10).unwrap_or(0);
let tmpl_sel = cpu.stack_word(mmu, 12).unwrap_or(0);
let this_sel = cpu.segment_selector(crate::emulator::emulator::isa_int::Seg::Es);
let this_off = cpu.regs.get16(Reg16::Bx);
let want = if tmpl_sel == 0 {
crate::format::ne::ResId::Int(tmpl_off)
} else {
let lin = cpu.far_to_linear(tmpl_sel, tmpl_off);
crate::format::ne::ResId::Name(
String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 256)).into_owned(),
)
};
let dlg = state
.resources
.iter()
.find(|r| r.type_id == crate::format::ne::ResId::Int(5) && res_id_eq(&r.name_id, &want))
.map(|r| r.data.clone());
let (title, controls) = dlg
.as_deref()
.map_or_else(|| (String::new(), Vec::new()), gui::parse_dialog_template);
state.gui.events.push(gui::GuiEvent::DialogStart {
title,
controls: controls.clone(),
});
if std::env::var("UD_NE_DRIVE_DIALOG").is_ok() {
let hdlg = state.gui.alloc_hwnd();
state.dialog_ended = false;
state.dialog_result = 0;
state.dialog_items.clear();
let dest_dir = std::env::var("UD_NE_INSTALL_DIR").unwrap_or_else(|_| "C:\\EXPANDER".into());
for c in &controls {
let initial = if c.class.eq_ignore_ascii_case("Edit") {
dest_dir.as_str()
} else {
c.text.as_str()
};
let chwnd = state
.gui
.create_window(&c.class, initial, hdlg, c.id, c.style);
state.dialog_items.insert(c.id, chwnd);
}
let attach_off = mfc_attach_offset();
call_guest_far16(
cpu,
mmu,
registry,
state,
proc_sel,
attach_off,
&[hdlg, this_sel, this_off],
)
.map_err(|e| Win32Error::InvalidArgument {
stub: "DialogBox/attach",
reason: e.to_string(),
})?;
deliver_message(
cpu, mmu, registry, state, proc_sel, proc_off, hdlg, 0x0030, 0, 0,
)?;
deliver_message(
cpu,
mmu,
registry,
state,
proc_sel,
proc_off,
hdlg,
WM_INITDIALOG,
0,
0,
)?;
let mut order: Vec<u16> = controls
.iter()
.filter(|c| c.class.eq_ignore_ascii_case("Button") && c.style & 0x000F == 1)
.map(|c| c.id)
.collect();
order.extend([1u16, 2u16]); let dbg = std::env::var("UD_NE_DOS_DEBUG").is_ok();
let (cmd_sel, cmd_off) = state
.gui
.classes
.get("AfxWnd")
.map_or((proc_sel, proc_off), |c| (c.wndproc_sel, c.wndproc_off));
for &id in &order {
if state.dialog_ended {
break;
}
let ctl = u32::from(state.dialog_items.get(&id).copied().unwrap_or(0));
let r = deliver_message(
cpu, mmu, registry, state, cmd_sel, cmd_off, hdlg, WM_COMMAND, id, ctl,
)?;
if dbg {
eprintln!(
"DRIVE WM_COMMAND id={id} -> {r:#x}, dialog_ended={}",
state.dialog_ended
);
}
}
return Ok(u32::from(state.dialog_result as u16));
}
let _ = (proc_sel, proc_off, this_sel, this_off);
Ok(1) }
fn mfc_attach_offset() -> u16 {
std::env::var("UD_NE_ATTACH_OFF")
.ok()
.and_then(|s| u16::from_str_radix(s.trim_start_matches("0x"), 16).ok())
.unwrap_or(0x91a6)
}
#[allow(clippy::too_many_arguments)]
fn deliver_message(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &mut Registry,
state: &mut HostState,
proc_sel: u16,
proc_off: u16,
hwnd: u16,
msg: u16,
wparam: u16,
lparam: u32,
) -> Result<u32, Win32Error> {
let args = [hwnd, msg, wparam, (lparam >> 16) as u16, lparam as u16];
call_guest_far16(cpu, mmu, registry, state, proc_sel, proc_off, &args).map_err(|e| {
Win32Error::InvalidArgument {
stub: "DialogBox/dlgproc",
reason: e.to_string(),
}
})
}
fn stub_end_dialog(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let result = cpu.stack_word(mmu, 4).unwrap_or(0);
if std::env::var("UD_NE_DOS_DEBUG").is_ok() {
eprintln!("EndDialog(result={result})");
}
state.dialog_ended = true;
state.dialog_result = result as i16;
Ok(1)
}
const ATOM_BASE: u16 = 0xC000;
fn stub_global_add_atom(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let lin = far_arg_linear(cpu, mmu, 4);
let s = String::from_utf8_lossy(&read_guest_cstr(mmu, lin, 256)).into_owned();
let atom = match state.atoms.iter().position(|a| a.eq_ignore_ascii_case(&s)) {
Some(i) => ATOM_BASE + i as u16,
None => {
state.atoms.push(s);
ATOM_BASE + (state.atoms.len() as u16 - 1)
}
};
Ok(u32::from(atom))
}
fn stub_get_win_flags(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0x0001 | 0x0004 | 0x0020 | 0x0400)
}
fn stub_get_current_task(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(u32::from(WIN16_HTASK))
}
fn stub_get_module_handle(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let v = cpu.stack_dword(mmu, 4).unwrap_or(0);
let selector = (v >> 16) as u16;
if selector == 0 {
return Ok(u32::from(v as u16));
}
Ok(u32::from(WIN16_HINSTANCE))
}
fn stub_load_string(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let n_max = cpu.stack_word(mmu, 4).unwrap_or(0);
let buf_lin = far_arg_linear(cpu, mmu, 6);
let uid = cpu.stack_word(mmu, 10).unwrap_or(0);
let text = state
.string_resources
.get(&uid)
.cloned()
.unwrap_or_default();
if n_max == 0 {
return Ok(0);
}
write_guest_cstr(mmu, buf_lin, n_max, text.as_bytes())
}
fn stub_get_file_title(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let cb_buf = cpu.stack_word(mmu, 4).unwrap_or(0);
let title_lin = far_arg_linear(cpu, mmu, 6);
let file_lin = far_arg_linear(cpu, mmu, 10);
let path = read_guest_cstr(mmu, file_lin, 0x1000);
let start = path
.iter()
.rposition(|&b| b == b'\\' || b == b'/' || b == b':')
.map_or(0, |p| p + 1);
let title = &path[start..];
if u32::from(cb_buf) <= title.len() as u32 {
return Ok(title.len() as u32 + 1);
}
write_guest_cstr(mmu, title_lin, cb_buf, title)?;
Ok(0)
}
fn stub_global_size(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let handle = cpu.stack_word(mmu, 4).unwrap_or(0);
let size = state
.win16_heap
.blocks
.get(&handle)
.map_or(0x1000, |&(_, s)| s);
Ok(size)
}
fn stub_global_alloc(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let flags = cpu.stack_word(mmu, 8).unwrap_or(0);
let size = cpu.stack_dword(mmu, 4).unwrap_or(0);
let sel = state.win16_heap.alloc(cpu, mmu, size);
if std::env::var("UD_NE_HEAP_DEBUG").is_ok() {
eprintln!(" GlobalAlloc(flags={flags:#06x}, size={size:#x}) → sel={sel:#06x}");
}
Ok(u32::from(sel))
}
fn stub_global_realloc(
cpu: &mut Cpu,
mmu: &mut Mmu,
state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let h_mem = cpu.stack_word(mmu, 10).unwrap_or(0);
let size = cpu.stack_dword(mmu, 6).unwrap_or(0);
let flags = cpu.stack_word(mmu, 4).unwrap_or(0);
let sel = state.win16_heap.realloc(cpu, mmu, h_mem, size);
if std::env::var("UD_NE_HEAP_DEBUG").is_ok() {
eprintln!(
" GlobalReAlloc(hMem={h_mem:#06x}, size={size:#x}, flags={flags:#06x}) \
→ sel={sel:#06x}"
);
}
Ok(u32::from(sel))
}
fn stub_global_lock(
cpu: &mut Cpu,
mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
let handle = cpu.stack_word(mmu, 4).unwrap_or(0);
Ok(u32::from(handle) << 16)
}
fn stub_lock_segment(
cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(u32::from(cpu.ds_selector()))
}
fn stub_ret0_1word(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0)
}
fn stub_get_version(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
Ok(0x0003_0A03)
}
fn stub_init_task(
cpu: &mut Cpu,
_mmu: &mut Mmu,
_state: &mut HostState,
_registry: &mut Registry,
) -> Result<u32, Win32Error> {
cpu.regs.set16(Reg16::Cx, 0);
cpu.regs.set16(Reg16::Si, 0);
cpu.regs.set16(Reg16::Di, WIN16_HINSTANCE);
cpu.regs.set16(Reg16::Bx, 0x80);
cpu.set_segment_reg(0 , crate::emulator::ne::PSP_SELECTOR);
Ok((u32::from(SW_SHOWNORMAL) << 16) | 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emulator::emulator::Cpu;
#[test]
fn dos_get_version_sets_al_and_clears_carry() {
let mut cpu = Cpu::new();
cpu.regs.flags.cf = true;
cpu.regs.set8(Reg8::Ah, 0x30); let mut mmu = Mmu::new();
let mut state = HostState::default();
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get8(Reg8::Al), 6, "DOS major version in AL");
assert!(!cpu.regs.flags.cf, "carry cleared on success");
}
#[test]
fn dos_get_current_drive_returns_c() {
let mut cpu = Cpu::new();
cpu.regs.set8(Reg8::Ah, 0x19);
let mut mmu = Mmu::new();
let mut state = HostState::default();
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get8(Reg8::Al), 2, "drive C: (0=A)");
}
#[test]
fn service_interrupt_handles_21h_and_rejects_unknown() {
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
cpu.regs.set8(Reg8::Ah, 0x30);
assert!(service_interrupt(0x21, &mut cpu, &mut mmu, &mut state));
assert!(!service_interrupt(0x80, &mut cpu, &mut mmu, &mut state));
}
fn set_dos_ds_dx_to_path(cpu: &mut Cpu, mmu: &mut Mmu, linear_base: u32, path: &str) {
use crate::emulator::emulator::mmu::Perm;
mmu.map(linear_base, 0x1000, Perm::R | Perm::W);
for (i, b) in path.bytes().enumerate() {
mmu.store8(linear_base + i as u32, b).unwrap();
}
mmu.store8(linear_base + path.len() as u32, 0).unwrap();
cpu.define_selector(1, linear_base);
cpu.load_segment(crate::emulator::emulator::isa_int::Seg::Ds, 1);
cpu.regs.set16(Reg16::Dx, 0);
}
#[test]
fn dos_open_create_rejects_empty_path() {
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(cpu.regs.flags.cf, "carry set on failed open");
assert_eq!(cpu.regs.get16(Reg16::Ax), 0x02, "file-not-found");
}
#[test]
fn dos_open_returns_handle_for_known_file() {
use crate::emulator::context::{FileAccess, VirtualFs};
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\hello.txt", b"hi".to_vec());
state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\HELLO.TXT");
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0); dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "carry clear on successful open");
let dh = cpu.regs.get16(Reg16::Bx); let dh_ax = cpu.regs.get16(Reg16::Ax);
assert!(dh_ax >= 5, "DOS handle starts at 5; got {dh_ax:#x}");
assert!(
state.dos_handles.contains_key(&dh_ax),
"handle table tracks it"
);
let _ = (FileAccess::Read, dh); }
#[test]
fn dos_read_returns_bytes_advances_position() {
use crate::emulator::context::VirtualFs;
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\data.bin", b"ABCDEFGH".to_vec());
state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\DATA.BIN");
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
let dh = cpu.regs.get16(Reg16::Ax);
cpu.regs.set8(Reg8::Ah, 0x3F);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 4);
cpu.regs.set16(Reg16::Dx, 0x100); dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "carry clear on successful read");
assert_eq!(cpu.regs.get16(Reg16::Ax), 4, "read 4 bytes");
let mut got = [0u8; 4];
for i in 0..4 {
got[i] = mmu.load8(0x10_0000 + 0x100 + i as u32).unwrap();
}
assert_eq!(&got, b"ABCD");
cpu.regs.set8(Reg8::Ah, 0x3F);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 4);
cpu.regs.set16(Reg16::Dx, 0x100);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get16(Reg16::Ax), 4, "read 4 more bytes");
for i in 0..4 {
got[i] = mmu.load8(0x10_0000 + 0x100 + i as u32).unwrap();
}
assert_eq!(&got, b"EFGH");
cpu.regs.set8(Reg8::Ah, 0x3F);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 4);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get16(Reg16::Ax), 0, "EOF read returns 0");
}
#[test]
fn dos_write_then_read_round_trips_through_vfs() {
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\OUT.BIN");
cpu.regs.set8(Reg8::Ah, 0x3C); cpu.regs.set16(Reg16::Cx, 0); dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "create succeeds");
let dh = cpu.regs.get16(Reg16::Ax);
for (i, b) in b"WXYZ".iter().enumerate() {
mmu.store8(0x10_0000 + 0x200 + i as u32, *b).unwrap();
}
cpu.regs.set8(Reg8::Ah, 0x40);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 4);
cpu.regs.set16(Reg16::Dx, 0x200);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "write succeeds");
assert_eq!(cpu.regs.get16(Reg16::Ax), 4, "wrote 4 bytes");
cpu.regs.set8(Reg8::Ah, 0x42);
cpu.regs.set8(Reg8::Al, 0); cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 0);
cpu.regs.set16(Reg16::Dx, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get16(Reg16::Ax), 0, "position low word = 0");
cpu.regs.set8(Reg8::Ah, 0x3F);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 4);
cpu.regs.set16(Reg16::Dx, 0x300);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get16(Reg16::Ax), 4, "read 4 bytes");
for (i, want) in b"WXYZ".iter().enumerate() {
assert_eq!(mmu.load8(0x10_0000 + 0x300 + i as u32).unwrap(), *want);
}
}
#[test]
fn dos_seek_end_returns_file_size() {
use crate::emulator::context::VirtualFs;
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\sized.bin", vec![0u8; 0x1234]); state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\SIZED.BIN");
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
let dh = cpu.regs.get16(Reg16::Ax);
cpu.regs.set8(Reg8::Ah, 0x42);
cpu.regs.set8(Reg8::Al, 2);
cpu.regs.set16(Reg16::Bx, dh);
cpu.regs.set16(Reg16::Cx, 0);
cpu.regs.set16(Reg16::Dx, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert_eq!(cpu.regs.get16(Reg16::Ax), 0x1234);
assert_eq!(cpu.regs.get16(Reg16::Dx), 0);
}
#[test]
fn dos_file_attribs_reports_archive_for_known_file() {
use crate::emulator::context::VirtualFs;
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\flag.txt", vec![0u8; 1]);
state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\FLAG.TXT");
cpu.regs.set8(Reg8::Ah, 0x43);
cpu.regs.set8(Reg8::Al, 0); dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "get-attrs succeeds for known file");
assert_eq!(cpu.regs.get16(Reg16::Cx), 0x20, "FILE_ATTRIBUTE_ARCHIVE");
}
#[test]
fn dos_file_attribs_fails_for_missing_file() {
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\NOPE.TXT");
cpu.regs.set8(Reg8::Ah, 0x43);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(cpu.regs.flags.cf, "get-attrs fails for unknown file");
assert_eq!(cpu.regs.get16(Reg16::Ax), 0x02, "file-not-found");
}
#[test]
fn dos_ioctl_get_info_reports_regular_file() {
use crate::emulator::context::VirtualFs;
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\f.bin", vec![0u8; 1]);
state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\F.BIN");
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
let dh = cpu.regs.get16(Reg16::Ax);
cpu.regs.set8(Reg8::Ah, 0x44);
cpu.regs.set8(Reg8::Al, 0); cpu.regs.set16(Reg16::Bx, dh);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "ioctl succeeds for known handle");
assert_eq!(
cpu.regs.get16(Reg16::Dx) & 0x8000,
0,
"DX bit 15 clear (file, not device)"
);
}
#[test]
fn dos_close_removes_handle() {
use crate::emulator::context::VirtualFs;
let mut cpu = Cpu::new();
let mut mmu = Mmu::new();
let mut state = HostState::default();
let mut vfs = VirtualFs::new();
vfs.insert("c:\\x.bin", vec![0u8]);
state.context.vfs = Some(crate::emulator::fsmount::MountTable::with_root(vfs));
set_dos_ds_dx_to_path(&mut cpu, &mut mmu, 0x10_0000, "C:\\X.BIN");
cpu.regs.set8(Reg8::Ah, 0x3D);
cpu.regs.set8(Reg8::Al, 0);
dos_int21(&mut cpu, &mut mmu, &mut state);
let dh = cpu.regs.get16(Reg16::Ax);
assert!(state.dos_handles.contains_key(&dh));
cpu.regs.set8(Reg8::Ah, 0x3E);
cpu.regs.set16(Reg16::Bx, dh);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(!cpu.regs.flags.cf, "close succeeds");
assert!(!state.dos_handles.contains_key(&dh), "handle removed");
cpu.regs.set8(Reg8::Ah, 0x3E);
cpu.regs.set16(Reg16::Bx, dh);
dos_int21(&mut cpu, &mut mmu, &mut state);
assert!(cpu.regs.flags.cf, "double-close errors");
assert_eq!(cpu.regs.get16(Reg16::Ax), 0x06, "invalid-handle code");
}
}