From 1918e1160b29b144758ae6675de359f82939346b Mon Sep 17 00:00:00 2001 From: GGRei Date: Tue, 26 May 2026 00:21:58 +0200 Subject: [PATCH] v2: link minimal Windows x64 PE executables for hello world! (#27262) --- vlib/builtin/builtin_nix.c.v | 7 + vlib/builtin/builtin_windows.c.v | 89 +++ vlib/builtin/printing.c.v | 65 +-- vlib/v2/abi/abi.v | 38 +- vlib/v2/abi/abi_test.v | 126 +++++ vlib/v2/builder/builder.v | 93 +++- vlib/v2/builder/cache_headers.v | 18 +- vlib/v2/builder/native_test.v | 51 ++ vlib/v2/builder/parse.v | 11 +- vlib/v2/builder/parse_d_parallel.v | 9 +- vlib/v2/builder/target_os_test.v | 482 +++++++++++++++++ vlib/v2/gen/x64/abi.v | 103 ++++ vlib/v2/gen/x64/asm.v | 22 + vlib/v2/gen/x64/coff.v | 257 +++++++++ vlib/v2/gen/x64/object.v | 37 ++ vlib/v2/gen/x64/pe.v | 474 ++++++++++++++++ vlib/v2/gen/x64/x64.v | 331 +++++++++--- vlib/v2/gen/x64/x64_abi_test.v | 653 +++++++++++++++++++++++ vlib/v2/gen/x64/x64_object_format_test.v | 150 ++++++ vlib/v2/gen/x64/x64_pe_linker_test.v | 363 +++++++++++++ vlib/v2/markused/markused.v | 52 +- vlib/v2/pref/comptime.v | 51 +- vlib/v2/pref/comptime_test.v | 45 ++ vlib/v2/pref/pref.v | 16 +- vlib/v2/ssa/builder.v | 77 ++- vlib/v2/transformer/transformer.v | 119 +++++ vlib/v2/types/checker.v | 100 +--- 27 files changed, 3528 insertions(+), 311 deletions(-) create mode 100644 vlib/v2/builder/native_test.v create mode 100644 vlib/v2/builder/target_os_test.v create mode 100644 vlib/v2/gen/x64/abi.v create mode 100644 vlib/v2/gen/x64/coff.v create mode 100644 vlib/v2/gen/x64/pe.v create mode 100644 vlib/v2/gen/x64/x64_abi_test.v create mode 100644 vlib/v2/gen/x64/x64_pe_linker_test.v create mode 100644 vlib/v2/pref/comptime_test.v diff --git a/vlib/builtin/builtin_nix.c.v b/vlib/builtin/builtin_nix.c.v index c0fba08bf..b6d0afd84 100644 --- a/vlib/builtin/builtin_nix.c.v +++ b/vlib/builtin/builtin_nix.c.v @@ -34,3 +34,10 @@ pub fn panic_lasterr(base string) { fn write_buf_to_console(fd int, buf &u8, buf_len int) bool { return false } + +// write_buf_to_fd_kernel32 is a Windows-only helper for the minimal V2 PE path. +// Keep a non-Windows stub so optional compile-time branches type-check while +// bootstrapping V on other hosts. +fn write_buf_to_fd_kernel32(fd int, buf &u8, buf_len int) bool { + return false +} diff --git a/vlib/builtin/builtin_windows.c.v b/vlib/builtin/builtin_windows.c.v index f8beca2b5..eed163f22 100644 --- a/vlib/builtin/builtin_windows.c.v +++ b/vlib/builtin/builtin_windows.c.v @@ -47,6 +47,10 @@ pub type C.LPTSTR = &C.TCHAR pub type C.LPCTSTR = &C.TCHAR fn C.WriteConsoleW(voidptr, &u16, u32, voidptr, voidptr) bool +fn C.WriteFile(voidptr, voidptr, u32, voidptr, voidptr) bool +fn C.GetProcessHeap() voidptr +fn C.HeapAlloc(voidptr, u32, usize) voidptr +fn C.HeapFree(voidptr, u32, voidptr) bool fn C._setmode(int, int) int @@ -116,6 +120,91 @@ fn write_buf_to_console(fd int, buf &u8, buf_len int) bool { } } +@[manualfree] +fn write_buf_to_console_kernel32(fd int, buf &u8, buf_len int) bool { + if buf_len <= 0 || (fd != 1 && fd != 2) { + return false + } + console_handle := C.GetStdHandle(if fd == 2 { std_error_handle } else { std_output_handle }) + if isnil(console_handle) { + return false + } + mut mode := u32(0) + if !C.GetConsoleMode(console_handle, &mode) { + return false + } + unsafe { + wide_len := C.MultiByteToWideChar(cp_utf8, 0, &char(buf), buf_len, 0, 0) + if wide_len <= 0 { + return false + } + heap := C.GetProcessHeap() + if isnil(heap) { + return false + } + wide_size := usize((wide_len + 1) * int(sizeof(u16))) + mut wide_buf := &u16(C.HeapAlloc(heap, 0, wide_size)) + if isnil(wide_buf) { + return false + } + defer { + C.HeapFree(heap, 0, wide_buf) + } + converted := C.MultiByteToWideChar(cp_utf8, 0, &char(buf), buf_len, wide_buf, wide_len) + if converted <= 0 { + return false + } + wide_buf[converted] = 0 + mut remaining_chars := converted + mut wide_ptr := wide_buf + for remaining_chars > 0 { + mut chars_written := u32(0) + if !C.WriteConsoleW(console_handle, wide_ptr, u32(remaining_chars), voidptr(&chars_written), nil) + || chars_written == 0 { + return false + } + wide_ptr += int(chars_written) + remaining_chars -= int(chars_written) + } + return true + } +} + +fn write_buf_to_fd_kernel32(fd int, buf &u8, buf_len int) bool { + if buf_len <= 0 { + return true + } + if fd != 1 && fd != 2 { + return false + } + if write_buf_to_console_kernel32(fd, buf, buf_len) { + return true + } + handle_id := if fd == 2 { std_error_handle } else { std_output_handle } + handle := C.GetStdHandle(handle_id) + if isnil(handle) { + return false + } + mut ptr := unsafe { buf } + mut remaining_bytes := buf_len + unsafe { + for remaining_bytes > 0 { + chunk := if remaining_bytes > int(0x7fffffff) { + int(0x7fffffff) + } else { + remaining_bytes + } + mut written := u32(0) + if !C.WriteFile(handle, ptr, u32(chunk), voidptr(&written), nil) || written == 0 { + return false + } + ptr += int(written) + remaining_bytes -= int(written) + } + } + return true +} + @[markused] fn builtin_init() { $if gcboehm ? { diff --git a/vlib/builtin/printing.c.v b/vlib/builtin/printing.c.v index 7cdae90ec..6b372049c 100644 --- a/vlib/builtin/printing.c.v +++ b/vlib/builtin/printing.c.v @@ -183,40 +183,45 @@ fn _write_buf_to_fd(fd int, buf &u8, buf_len int) { mut ptr := unsafe { buf } mut remaining_bytes := isize(buf_len) mut x := isize(0) - $if windows { - if write_buf_to_console(fd, ptr, int(remaining_bytes)) { - return + $if v2_native_windows_pe_minimal ? { + write_buf_to_fd_kernel32(fd, ptr, int(remaining_bytes)) + return + } $else { + $if windows { + if write_buf_to_console(fd, ptr, int(remaining_bytes)) { + return + } } - } - $if freestanding || vinix || builtin_write_buf_to_fd_should_use_c_write ? { - // Flush any pending libc stdio output (from C.puts, C.putchar, etc.) - // before writing directly via write() syscall to prevent output reordering. - C.fflush(unsafe { nil }) - unsafe { - for remaining_bytes > 0 { - x = C.write(fd, ptr, remaining_bytes) - if x <= 0 { - // Detached/invalid stdio must not trap the process in an infinite loop. - break + $if freestanding || vinix || builtin_write_buf_to_fd_should_use_c_write ? { + // Flush any pending libc stdio output (from C.puts, C.putchar, etc.) + // before writing directly via write() syscall to prevent output reordering. + C.fflush(unsafe { nil }) + unsafe { + for remaining_bytes > 0 { + x = C.write(fd, ptr, remaining_bytes) + if x <= 0 { + // Detached/invalid stdio must not trap the process in an infinite loop. + break + } + ptr += x + remaining_bytes -= x } - ptr += x - remaining_bytes -= x } - } - } $else { - mut stream := voidptr(C.stdout) - if fd == 2 { - stream = voidptr(C.stderr) - } - unsafe { - for remaining_bytes > 0 { - x = isize(C.fwrite(ptr, 1, remaining_bytes, stream)) - if x <= 0 { - // GUI programs on Windows may not have a writable stdout/stderr stream. - break + } $else { + mut stream := voidptr(C.stdout) + if fd == 2 { + stream = voidptr(C.stderr) + } + unsafe { + for remaining_bytes > 0 { + x = isize(C.fwrite(ptr, 1, remaining_bytes, stream)) + if x <= 0 { + // GUI programs on Windows may not have a writable stdout/stderr stream. + break + } + ptr += x + remaining_bytes -= x } - ptr += x - remaining_bytes -= x } } } diff --git a/vlib/v2/abi/abi.v b/vlib/v2/abi/abi.v index 7f7dc7da1..2f53fc31e 100644 --- a/vlib/v2/abi/abi.v +++ b/vlib/v2/abi/abi.v @@ -7,10 +7,19 @@ module abi import v2.mir import v2.pref +pub enum X64Abi { + sysv + windows +} + // lower annotates MIR with ABI classification metadata. // Current scope is intentionally conservative: it classifies which arguments // and return values must be passed indirectly for each function. pub fn lower(mut m mir.Module, arch pref.Arch) { + lower_with_x64_abi(mut m, arch, .sysv) +} + +pub fn lower_with_x64_abi(mut m mir.Module, arch pref.Arch, x64_abi X64Abi) { mut fn_by_name := map[string]int{} for i := 0; i < m.funcs.len; i++ { mut f := &m.funcs[i] @@ -21,17 +30,17 @@ pub fn lower(mut m mir.Module, arch pref.Arch) { continue } param_typ := m.values[param_id].typ - if needs_indirect(m, param_typ, arch) { + if needs_indirect(m, param_typ, arch, x64_abi) { f.abi_param_class[pi] = .indirect } } - f.abi_ret_indirect = needs_indirect(m, f.typ, arch) + f.abi_ret_indirect = needs_indirect(m, f.typ, arch, x64_abi) } - lower_calls(mut m, arch, fn_by_name) + lower_calls(mut m, arch, x64_abi, fn_by_name) } -fn needs_indirect(m mir.Module, typ_id int, arch pref.Arch) bool { +fn needs_indirect(m mir.Module, typ_id int, arch pref.Arch, x64_abi X64Abi) bool { ssa_mod := m.ssa() if ssa_mod == unsafe { nil } || typ_id <= 0 || typ_id >= ssa_mod.type_store.types.len { return false @@ -42,9 +51,18 @@ fn needs_indirect(m mir.Module, typ_id int, arch pref.Arch) bool { } size := m.type_size(typ_id) return match arch { - .arm64 { size > 16 } - .x64 { size > 16 } - else { size > 16 } + .arm64 { + size > 16 + } + .x64 { + match x64_abi { + .sysv { size > 16 } + .windows { size !in [1, 2, 4, 8] } + } + } + else { + size > 16 + } } } @@ -97,7 +115,7 @@ fn logical_arg_type_from_value(m mir.Module, val_id int, depth int) ?int { return none } -fn lower_calls(mut m mir.Module, arch pref.Arch, fn_by_name map[string]int) { +fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[string]int) { if arch !in [.arm64, .x64] { return } @@ -118,12 +136,12 @@ fn lower_calls(mut m mir.Module, arch pref.Arch, fn_by_name map[string]int) { arg_id := instr.operands[arg_idx + 1] arg_typ = fallback_arg_type(m, arg_id) } - if needs_indirect(m, arg_typ, arch) { + if needs_indirect(m, arg_typ, arch, x64_abi) { instr.abi_arg_class[arg_idx] = .indirect } } - instr.abi_ret_indirect = needs_indirect(m, ret_typ, arch) + instr.abi_ret_indirect = needs_indirect(m, ret_typ, arch, x64_abi) // Lower ABI-indirect returns to call_sret for backend consumption. if instr.abi_ret_indirect { instr.op = .call_sret diff --git a/vlib/v2/abi/abi_test.v b/vlib/v2/abi/abi_test.v index 8c8ae769e..c8e7d00c8 100644 --- a/vlib/v2/abi/abi_test.v +++ b/vlib/v2/abi/abi_test.v @@ -113,6 +113,132 @@ fn test_x64_large_struct_call_is_lowered_to_call_sret() { assert call_instr.op == .call_sret } +fn test_x64_sysv_sixteen_byte_struct_return_stays_direct() { + mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_16_ret') + i64_t := ssa_mod.type_store.get_int(64) + struct16_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i64_t] + }) + + callee_id := ssa_mod.new_function('callee', struct16_t, []) + callee_entry := ssa_mod.add_block(callee_id, 'entry') + zero := ssa_mod.get_or_add_const(i64_t, '0') + ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) + + caller_id := ssa_mod.new_function('caller', i64_t, []) + caller_entry := ssa_mod.add_block(caller_id, 'entry') + fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) + call_val := ssa_mod.add_instr(.call, caller_entry, struct16_t, [fn_val]) + ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) + + mut mir_mod := mir.lower_from_ssa(ssa_mod) + lower(mut mir_mod, .x64) + + call_instr := mir_mod.instrs[mir_mod.values[call_val].index] + assert call_instr.op == .call + assert !call_instr.abi_ret_indirect +} + +fn test_x64_windows_struct_return_classification() { + mut ssa_mod := ssa.Module.new('abi_test_x64_windows_ret') + i64_t := ssa_mod.type_store.get_int(64) + i8_t := ssa_mod.type_store.get_int(8) + struct8_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t] + }) + struct9_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i8_t] + }) + struct16_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i64_t] + }) + + fn8 := ssa_mod.new_function('ret8', struct8_t, []) + fn9 := ssa_mod.new_function('ret9', struct9_t, []) + fn16 := ssa_mod.new_function('ret16', struct16_t, []) + + mut mir_mod := mir.lower_from_ssa(ssa_mod) + lower_with_x64_abi(mut mir_mod, .x64, .windows) + + assert !mir_mod.funcs[fn8].abi_ret_indirect + assert mir_mod.funcs[fn9].abi_ret_indirect + assert mir_mod.funcs[fn16].abi_ret_indirect +} + +fn test_x64_windows_struct_param_classification() { + mut ssa_mod := ssa.Module.new('abi_test_x64_windows_param') + i64_t := ssa_mod.type_store.get_int(64) + i8_t := ssa_mod.type_store.get_int(8) + struct8_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t] + }) + struct9_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i8_t] + }) + struct16_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i64_t] + }) + + fn_id := ssa_mod.new_function('callee', i64_t, []) + param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0) + param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0) + param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0) + ssa_mod.funcs[fn_id].params << [param8, param9, param16] + + mut mir_mod := mir.lower_from_ssa(ssa_mod) + lower_with_x64_abi(mut mir_mod, .x64, .windows) + + assert mir_mod.funcs[fn_id].abi_param_class == [.in_reg, .indirect, .indirect] +} + +fn test_x64_windows_callsite_struct_arg_classification() { + mut ssa_mod := ssa.Module.new('abi_test_x64_windows_call_arg') + i64_t := ssa_mod.type_store.get_int(64) + i8_t := ssa_mod.type_store.get_int(8) + struct8_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t] + }) + struct9_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i8_t] + }) + struct16_t := ssa_mod.type_store.register(ssa.Type{ + kind: .struct_t + fields: [i64_t, i64_t] + }) + + callee_id := ssa_mod.new_function('callee', i64_t, []) + param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0) + param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0) + param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0) + ssa_mod.funcs[callee_id].params << [param8, param9, param16] + + caller_id := ssa_mod.new_function('caller', i64_t, []) + arg8 := ssa_mod.add_value_node(.argument, struct8_t, 'arg8', 0) + arg9 := ssa_mod.add_value_node(.argument, struct9_t, 'arg9', 0) + arg16 := ssa_mod.add_value_node(.argument, struct16_t, 'arg16', 0) + ssa_mod.funcs[caller_id].params << [arg8, arg9, arg16] + caller_entry := ssa_mod.add_block(caller_id, 'entry') + fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) + call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, arg8, arg9, arg16]) + zero := ssa_mod.get_or_add_const(i64_t, '0') + ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) + + mut mir_mod := mir.lower_from_ssa(ssa_mod) + lower_with_x64_abi(mut mir_mod, .x64, .windows) + + call_instr := mir_mod.instrs[mir_mod.values[call_val].index] + assert call_instr.abi_arg_class == [.in_reg, .indirect, .indirect] +} + fn test_arm64_external_large_struct_call_is_lowered_to_call_sret() { mut ssa_mod := ssa.Module.new('abi_test_external') i64_t := ssa_mod.type_store.get_int(64) diff --git a/vlib/v2/builder/builder.v b/vlib/v2/builder/builder.v index 4734032c7..9e2787573 100644 --- a/vlib/v2/builder/builder.v +++ b/vlib/v2/builder/builder.v @@ -150,7 +150,13 @@ pub fn (mut b Builder) build(files []string) { b.used_fn_keys = map[string]bool{} } else { mark_used_start := sw.elapsed() - b.used_fn_keys = markused.mark_used(b.files, b.env) + if b.uses_minimal_windows_x64_runtime() { + b.used_fn_keys = markused.mark_used_with_options(b.files, b.env, markused.MarkUsedOptions{ + minimal_runtime_roots: true + }) + } else { + b.used_fn_keys = markused.mark_used(b.files, b.env) + } mark_used_time := time.Duration(sw.elapsed() - mark_used_start) print_time('Mark Used', mark_used_time) } @@ -409,6 +415,7 @@ fn (mut b Builder) gen_ssa_c() { return } mut ssa_builder := ssa.Builder.new_with_env(mod, b.env) + ssa_builder.target_os = b.pref.target_os_or_host() mut stage_start := sw.elapsed() ssa_builder.build_all(b.files) @@ -1299,6 +1306,41 @@ fn normalize_target_os_name(target_os string) string { } } +fn is_windows_x64_native_target(arch pref.Arch, target_os string) bool { + return arch == .x64 && normalize_target_os_name(target_os) == 'windows' +} + +fn (b &Builder) uses_minimal_windows_x64_runtime() bool { + arch := b.pref.get_effective_arch() + return b.pref.backend == .x64 && is_windows_x64_native_target(arch, b.pref.target_os_or_host()) +} + +fn is_macos_native_target(target_os string) bool { + return normalize_target_os_name(target_os) == 'macos' +} + +fn native_x64_object_format_for_os(target_os string) x64.ObjectFormat { + return match normalize_target_os_name(target_os) { + 'macos' { x64.ObjectFormat.macho } + 'windows' { x64.ObjectFormat.coff } + else { x64.ObjectFormat.elf } + } +} + +fn native_x64_codegen_abi_for_os(target_os string) x64.X64Abi { + return match normalize_target_os_name(target_os) { + 'windows' { x64.X64Abi.windows } + else { x64.X64Abi.sysv } + } +} + +fn native_x64_lowering_abi_for_os(target_os string) abi.X64Abi { + return match normalize_target_os_name(target_os) { + 'windows' { abi.X64Abi.windows } + else { abi.X64Abi.sysv } + } +} + fn flag_os_matches(cond string, target_os string) bool { current := normalize_target_os_name(target_os) return match cond.to_lower() { @@ -1504,9 +1546,10 @@ fn (b &Builder) collect_cflags_from_sources() string { } } if !b.pref.skip_builtin { + target_os := b.pref.target_os_or_host() for module_path in core_cached_module_paths { vlib_path := b.pref.get_vlib_module_path(module_path) - module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, os.user_os()) + module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, target_os) for mf in module_files { if mf !in scanned_files { scan_paths << mf @@ -1515,6 +1558,7 @@ fn (b &Builder) collect_cflags_from_sources() string { } } scan_paths.sort() + target_os := b.pref.target_os_or_host() for scan_path in scan_paths { if scan_path == '' || scan_path in scanned_files { continue @@ -1547,7 +1591,7 @@ fn (b &Builder) collect_cflags_from_sources() string { } if chain_matched[cur] { skip_depth = 1 - } else if comptime_cond_matches(new_cond, os.user_os()) { + } else if comptime_cond_matches(new_cond, target_os) { chain_matched[cur] = true skip_depth = 0 } else { @@ -1575,7 +1619,7 @@ fn (b &Builder) collect_cflags_from_sources() string { // $if cond { (chain opener) if rest.starts_with(r'$if ') { cond := rest[4..].trim_right('?{ ').trim_space() - matched := comptime_cond_matches(cond, os.user_os()) + matched := comptime_cond_matches(cond, target_os) chain_matched << matched if skip_depth > 0 { skip_depth++ @@ -1611,7 +1655,7 @@ fn (b &Builder) collect_cflags_from_sources() string { // Replace @VEXEROOT before parsing so path normalization sees absolute paths resolved_line := line.replace('@VEXEROOT', b.pref.vroot).replace('VEXEROOT', b.pref.vroot) - mut flag := parse_flag_directive_line(resolved_line, scan_path, os.user_os()) or { + mut flag := parse_flag_directive_line(resolved_line, scan_path, target_os) or { continue } // Build include flags from already-collected flags for compiling missing .o files @@ -1897,6 +1941,7 @@ fn run_cc_cmd_or_exit(cmd string, stage string, show_cc bool) bool { fn (mut b Builder) gen_native(backend_arch pref.Arch) { arch := if backend_arch == .auto { b.pref.get_effective_arch() } else { backend_arch } + target_os := b.pref.target_os_or_host() // Build all files into a single SSA module mut mod := ssa.Module.new('main') @@ -1907,6 +1952,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } mut ssa_builder := ssa.Builder.new_with_env(mod, b.env) ssa_builder.guard_invalid_type_payloads = true + ssa_builder.target_os = target_os + ssa_builder.minimal_runtime_roots = b.uses_minimal_windows_x64_runtime() mut native_sw := time.new_stopwatch() // Pass markused data for dead code elimination. The ARM64 backend has its own @@ -2011,7 +2058,11 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { print_time('MIR Lower', time.Duration(native_sw.elapsed() - stage_start)) stage_start = native_sw.elapsed() - abi.lower(mut mir_mod, arch) + if is_windows_x64_native_target(arch, target_os) { + abi.lower_with_x64_abi(mut mir_mod, arch, native_x64_lowering_abi_for_os(target_os)) + } else { + abi.lower(mut mir_mod, arch) + } print_time('ABI Lower', time.Duration(native_sw.elapsed() - stage_start)) stage_start = native_sw.elapsed() @@ -2027,13 +2078,7 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { 'out' } - if arch == .x64 && os.user_os() == 'windows' { - eprintln('error: the v2 x64 native backend does not support Windows yet') - eprintln('hint: Windows x64 requires COFF object output and the Windows x64 calling convention') - exit(1) - } - - if arch == .arm64 && os.user_os() == 'macos' { + if arch == .arm64 && is_macos_native_target(target_os) { // Use built-in linker for ARM64 macOS stage_start = native_sw.elapsed() mut gen := arm64.Gen.new(&mir_mod) @@ -2072,13 +2117,21 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } gen.write_file(obj_file) } else { - obj_format := if os.user_os() == 'macos' { - x64.ObjectFormat.macho - } else { - x64.ObjectFormat.elf - } - mut gen := x64.Gen.new_with_format(&mir_mod, obj_format) + obj_format := native_x64_object_format_for_os(target_os) + codegen_abi := native_x64_codegen_abi_for_os(target_os) + mut gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format, codegen_abi) gen.gen() + if is_windows_x64_native_target(arch, target_os) { + gen.link_executable(output_binary) or { + eprintln('Link failed:') + eprintln(err.msg()) + exit(1) + } + if b.pref.verbose { + println('[*] Linked ${output_binary} (built-in PE linker)') + } + return + } gen.write_file(obj_file) } @@ -2087,7 +2140,7 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } // Link the object file into an executable - if os.user_os() == 'macos' { + if is_macos_native_target(target_os) { sdk_res := os.execute('xcrun -sdk macosx --show-sdk-path') sdk_path := sdk_res.output.trim_space() arch_flag := if arch == .arm64 { 'arm64' } else { 'x86_64' } diff --git a/vlib/v2/builder/cache_headers.v b/vlib/v2/builder/cache_headers.v index 863589e40..bf34e097d 100644 --- a/vlib/v2/builder/cache_headers.v +++ b/vlib/v2/builder/cache_headers.v @@ -368,7 +368,8 @@ fn (b &Builder) source_files_for_module_name(module_name string) []string { if files_set.len == 0 { module_path := b.module_name_to_path(module_name) module_dir := b.pref.get_vlib_module_path(module_path) - for file in get_v_files_from_dir(module_dir, b.pref.user_defines, os.user_os()) { + for file in get_v_files_from_dir(module_dir, b.pref.user_defines, + b.pref.target_os_or_host()) { files_set[file] = true } } @@ -385,7 +386,7 @@ fn (b &Builder) core_cache_compiler_dependency_files() []string { if !os.is_dir(dir) { continue } - for file in get_v_files_from_dir(dir, b.pref.user_defines, os.user_os()) { + for file in get_v_files_from_dir(dir, b.pref.user_defines, b.pref.target_os_or_host()) { files_set[os.norm_path(file)] = true } } @@ -417,7 +418,8 @@ fn (b &Builder) user_entry_stamp_files() []string { if found_parsed_files { continue } - for source_file in get_user_v_files_from_dir(file, user_defines, os.user_os()) { + for source_file in get_user_v_files_from_dir(file, user_defines, + b.pref.target_os_or_host()) { if source_file == '' || source_file.ends_with('.vh') { continue } @@ -613,6 +615,7 @@ fn (b &Builder) cache_stamp_for_modules(cache_name string, modules []string, cc lines << 'cc_link_flags=${cc_link_flags}' lines << 'use_markused=${use_markused}' lines << 'context_alloc=${b.pref.use_context_allocator}' + lines << 'target_os=${b.pref.target_os_or_host()}' // Include user entry files in cache stamp: the transformer injects // helper functions (str, eq, sort comparators) into builtin module AST // based on types from the user's source file. Different source files @@ -647,6 +650,7 @@ fn (b &Builder) header_stamp_for_modules(modules []string) string { entry_files := b.user_entry_stamp_files() mut lines := []string{cap: source_files.len + compiler_files.len + entry_files.len + 4} lines << 'format=${core_headers_format}' + lines << 'target_os=${b.pref.target_os_or_host()}' for file in entry_files { lines << 'entry:${file}:${os.file_last_mod_unix(file)}' } @@ -684,6 +688,7 @@ fn (b &Builder) cache_stamp_for_parsed_modules(cache_name string, module_names [ lines << 'cc_link_flags=${cc_link_flags}' lines << 'use_markused=${use_markused}' lines << 'context_alloc=${b.pref.use_context_allocator}' + lines << 'target_os=${b.pref.target_os_or_host()}' for module_name in module_names { lines << 'module:${module_name}' } @@ -713,6 +718,7 @@ fn (b &Builder) cache_stamp_for_virtual_modules(groups []CachedVirtualModule, de lines << 'cc_link_flags=${cc_link_flags}' lines << 'use_markused=${use_markused}' lines << 'context_alloc=${b.pref.use_context_allocator}' + lines << 'target_os=${b.pref.target_os_or_host()}' for group in groups { lines << 'virtual:${group.name}:${group.header_name}' } @@ -735,6 +741,7 @@ fn (b &Builder) imports_header_stamp_for_modules(imports []CachedImportModule, m mut lines := []string{cap: source_files.len + compiler_files.len + imports.len + 6} lines << 'cache=${imports_cache_name}' lines << 'format=${core_headers_format}' + lines << 'target_os=${b.pref.target_os_or_host()}' for import_mod in imports { lines << 'import:${import_mod.import_path}:${import_mod.module_name}' } @@ -756,6 +763,7 @@ fn (b &Builder) virtuals_header_stamp_for_modules(groups []CachedVirtualModule) mut lines := []string{cap: source_files.len + compiler_files.len + groups.len + 6} lines << 'cache=${virtuals_cache_name}' lines << 'format=${core_headers_format}' + lines << 'target_os=${b.pref.target_os_or_host()}' for group in groups { lines << 'virtual:${group.name}:${group.header_name}' } @@ -1063,6 +1071,7 @@ fn (b &Builder) can_use_cached_module_bundle_for_parse(cache_name string, use_ma return stamp.contains('cache=${cache_name}\n') && stamp.contains('format=${core_cache_format}\n') && stamp.contains('use_markused=${use_markused}\n') + && stamp.contains('target_os=${b.pref.target_os_or_host()}\n') } fn (mut b Builder) ensure_core_module_headers() { @@ -1244,7 +1253,8 @@ fn (b &Builder) import_modules_for_cached_modules(module_names []string) []Cache mut import_set := map[string]bool{} mut imports := []CachedImportModule{} for file in b.files { - for import_stmt in active_file_imports(file, b.pref.user_defines, os.user_os()) { + for import_stmt in active_file_imports(file, b.pref.user_defines, + b.pref.target_os_or_host()) { module_name := import_stmt.name.all_after_last('.') if module_name !in module_set { continue diff --git a/vlib/v2/builder/native_test.v b/vlib/v2/builder/native_test.v new file mode 100644 index 000000000..babd6b863 --- /dev/null +++ b/vlib/v2/builder/native_test.v @@ -0,0 +1,51 @@ +module builder + +import v2.abi +import v2.gen.x64 +import v2.pref + +fn test_native_x64_windows_selects_coff_and_windows_abi() { + assert is_windows_x64_native_target(.x64, 'windows') + assert native_x64_object_format_for_os('windows') == x64.ObjectFormat.coff + assert native_x64_codegen_abi_for_os('windows') == x64.X64Abi.windows + assert native_x64_lowering_abi_for_os('windows') == abi.X64Abi.windows +} + +fn test_native_x64_non_windows_keeps_existing_object_formats_and_sysv() { + assert !is_windows_x64_native_target(.x64, 'linux') + assert native_x64_object_format_for_os('linux') == x64.ObjectFormat.elf + assert native_x64_codegen_abi_for_os('linux') == x64.X64Abi.sysv + assert native_x64_lowering_abi_for_os('linux') == abi.X64Abi.sysv + + assert !is_windows_x64_native_target(.x64, 'macos') + assert native_x64_object_format_for_os('macos') == x64.ObjectFormat.macho + assert native_x64_codegen_abi_for_os('macos') == x64.X64Abi.sysv + assert native_x64_lowering_abi_for_os('macos') == abi.X64Abi.sysv + assert is_macos_native_target('macos') + assert is_macos_native_target('darwin') + assert native_x64_object_format_for_os('darwin') == x64.ObjectFormat.macho + assert native_x64_codegen_abi_for_os('darwin') == x64.X64Abi.sysv + assert native_x64_lowering_abi_for_os('darwin') == abi.X64Abi.sysv +} + +fn test_minimal_windows_runtime_is_not_enabled_for_linux_or_macos() { + mut prefs := pref.new_preferences() + prefs.backend = .x64 + prefs.arch = .x64 + + prefs.target_os = 'linux' + linux_builder := new_builder(&prefs) + assert !linux_builder.uses_minimal_windows_x64_runtime() + + prefs.target_os = 'macos' + macos_builder := new_builder(&prefs) + assert !macos_builder.uses_minimal_windows_x64_runtime() + + prefs.target_os = 'darwin' + darwin_builder := new_builder(&prefs) + assert !darwin_builder.uses_minimal_windows_x64_runtime() + + prefs.target_os = 'windows' + windows_builder := new_builder(&prefs) + assert windows_builder.uses_minimal_windows_x64_runtime() +} diff --git a/vlib/v2/builder/parse.v b/vlib/v2/builder/parse.v index 1f8621acb..c8ff10661 100644 --- a/vlib/v2/builder/parse.v +++ b/vlib/v2/builder/parse.v @@ -211,6 +211,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { mut parser_reused := parser.Parser.new(b.pref) mut ast_files := []ast.File{} skip_builtin := b.pref.skip_builtin + target_os := b.pref.target_os_or_host() mut use_core_headers := false if !skip_builtin { use_core_headers = b.can_use_cached_core_headers_for_parse() @@ -222,7 +223,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { } else { for module_path in core_cached_module_paths { vlib_path := b.pref.get_vlib_module_path(module_path) - module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, os.user_os()) + module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, target_os) parsed_module_files := parser_reused.parse_files(module_files, mut b.file_set) ast_files << parsed_module_files } @@ -236,7 +237,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { continue } if os.is_dir(input) { - dir_files := get_user_v_files_from_dir(input, b.pref.user_defines, os.user_os()) + dir_files := get_user_v_files_from_dir(input, b.pref.user_defines, target_os) for dir_file in dir_files { if dir_file != '' && dir_file !in seen_user_files { expanded_user_files << dir_file @@ -250,7 +251,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { expanded_user_files << input seen_user_files[input] = true } - dir_files := get_v_files_from_dir(os.dir(input), b.pref.user_defines, os.user_os()) + dir_files := get_v_files_from_dir(os.dir(input), b.pref.user_defines, target_os) for dir_file in dir_files { if dir_file != '' && dir_file !in seen_user_files { expanded_user_files << dir_file @@ -288,7 +289,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { } for afi := 0; afi < ast_files.len; afi++ { ast_file := ast_files[afi] - for mod in active_file_imports(ast_file, b.pref.user_defines, os.user_os()) { + for mod in active_file_imports(ast_file, b.pref.user_defines, target_os) { if mod.name in parsed_imports { continue } @@ -301,7 +302,7 @@ fn (mut b Builder) parse_files(files []string) []ast.File { } } mod_path := b.pref.get_module_path(mod.name, ast_file.name) - module_files := get_v_files_from_dir(mod_path, b.pref.user_defines, os.user_os()) + module_files := get_v_files_from_dir(mod_path, b.pref.user_defines, target_os) if module_files.len == 0 { continue } diff --git a/vlib/v2/builder/parse_d_parallel.v b/vlib/v2/builder/parse_d_parallel.v index ecc8fd356..16cd09dc6 100644 --- a/vlib/v2/builder/parse_d_parallel.v +++ b/vlib/v2/builder/parse_d_parallel.v @@ -3,7 +3,6 @@ // that can be found in the LICENSE file. module builder -import os import v2.ast import v2.pref import v2.parser @@ -34,19 +33,20 @@ fn (mut pstate ParsingSharedState) already_parsed_module(name string) bool { fn worker(mut wp util.WorkerPool[string, ast.File], mut pstate ParsingSharedState, prefs &pref.Preferences) { mut p := parser.Parser.new(prefs) + target_os := prefs.target_os_or_host() for { filename := wp.get_job() or { break } ast_file := p.parse_file(filename, mut pstate.file_set) // Queue new jobs for imports before pushing result skip_imports := prefs.skip_imports if !skip_imports { - for mod in ast_file.imports { + for mod in active_file_imports(ast_file, prefs.user_defines, target_os) { if pstate.already_parsed_module(mod.name) { continue } pstate.mark_module_as_parsed(mod.name) mod_path := prefs.get_module_path(mod.name, ast_file.name) - wp.queue_jobs(get_v_files_from_dir(mod_path, prefs.user_defines, os.user_os())) + wp.queue_jobs(get_v_files_from_dir(mod_path, prefs.user_defines, target_os)) } } wp.push_result(ast_file) @@ -77,9 +77,10 @@ fn (mut b Builder) parse_files_parallel(files []string) []ast.File { if use_core_headers2 { worker_pool.queue_jobs(b.core_cached_parse_paths()) } else { + target_os := b.pref.target_os_or_host() for module_path in core_cached_module_paths { worker_pool.queue_jobs(get_v_files_from_dir(b.pref.get_vlib_module_path(module_path), - b.pref.user_defines, os.user_os())) + b.pref.user_defines, target_os)) } } } diff --git a/vlib/v2/builder/target_os_test.v b/vlib/v2/builder/target_os_test.v new file mode 100644 index 000000000..7e1056213 --- /dev/null +++ b/vlib/v2/builder/target_os_test.v @@ -0,0 +1,482 @@ +module builder + +import os +import v2.ast +import v2.abi +import v2.gen.x64 +import v2.insel +import v2.markused +import v2.mir +import v2.pref +import v2.ssa +import v2.ssa.optimize as ssa_optimize +import v2.transformer +import v2.types + +fn write_test_file(path string) { + os.write_file(path, 'module main\nfn marker() {}\n') or { panic(err) } +} + +fn test_get_v_files_from_dir_uses_windows_target_os() { + tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_filter_windows_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + + write_test_file(os.join_path(tmp_dir, 'common.v')) + write_test_file(os.join_path(tmp_dir, 'platform_windows.v')) + write_test_file(os.join_path(tmp_dir, 'platform_nix.v')) + write_test_file(os.join_path(tmp_dir, 'platform_linux.v')) + write_test_file(os.join_path(tmp_dir, 'platform_darwin.v')) + + names := get_v_files_from_dir(tmp_dir, []string{}, 'windows').map(os.file_name(it)) + assert 'common.v' in names + assert 'platform_windows.v' in names + assert 'platform_nix.v' !in names + assert 'platform_linux.v' !in names + assert 'platform_darwin.v' !in names +} + +fn test_get_v_files_from_dir_uses_linux_and_macos_target_os() { + tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_filter_unix_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + + write_test_file(os.join_path(tmp_dir, 'common.v')) + write_test_file(os.join_path(tmp_dir, 'platform_windows.v')) + write_test_file(os.join_path(tmp_dir, 'platform_nix.v')) + write_test_file(os.join_path(tmp_dir, 'platform_linux.v')) + write_test_file(os.join_path(tmp_dir, 'platform_macos.v')) + write_test_file(os.join_path(tmp_dir, 'platform_darwin.v')) + + linux_names := get_v_files_from_dir(tmp_dir, []string{}, 'linux').map(os.file_name(it)) + assert 'common.v' in linux_names + assert 'platform_nix.v' in linux_names + assert 'platform_linux.v' in linux_names + assert 'platform_windows.v' !in linux_names + assert 'platform_macos.v' !in linux_names + assert 'platform_darwin.v' !in linux_names + + macos_names := get_v_files_from_dir(tmp_dir, []string{}, 'macos').map(os.file_name(it)) + assert 'common.v' in macos_names + assert 'platform_nix.v' in macos_names + assert 'platform_macos.v' in macos_names + assert 'platform_darwin.v' in macos_names + assert 'platform_windows.v' !in macos_names + assert 'platform_linux.v' !in macos_names +} + +fn test_parse_files_uses_target_os_preference_for_windows_files() { + tmp_dir := os.join_path(os.temp_dir(), 'v2_builder_parse_windows_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + + os.write_file(os.join_path(tmp_dir, 'main.v'), 'module main\nfn main() {}\n') or { panic(err) } + write_test_file(os.join_path(tmp_dir, 'platform_windows.v')) + write_test_file(os.join_path(tmp_dir, 'platform_nix.v')) + write_test_file(os.join_path(tmp_dir, 'platform_linux.v')) + write_test_file(os.join_path(tmp_dir, 'platform_darwin.v')) + + mut prefs := pref.new_preferences() + prefs.skip_builtin = true + prefs.skip_imports = true + prefs.target_os = 'windows' + mut b := new_builder(&prefs) + files := b.parse_files([tmp_dir]) + names := files.map(os.file_name(it.name)) + + assert 'main.v' in names + assert 'platform_windows.v' in names + assert 'platform_nix.v' !in names + assert 'platform_linux.v' !in names + assert 'platform_darwin.v' !in names +} + +fn test_active_file_imports_filters_conditional_imports_by_target_os() { + file := ast.File{ + mod: 'main' + name: 'main.v' + stmts: [ + ast.Stmt(ast.ExprStmt{ + expr: ast.ComptimeExpr{ + expr: ast.IfExpr{ + cond: ast.Ident{ + name: 'linux' + } + stmts: [ + ast.Stmt(ast.ImportStmt{ + name: 'linmod' + }), + ] + } + } + }), + ast.Stmt(ast.ExprStmt{ + expr: ast.ComptimeExpr{ + expr: ast.IfExpr{ + cond: ast.Ident{ + name: 'windows' + } + stmts: [ + ast.Stmt(ast.ImportStmt{ + name: 'winmod' + }), + ] + } + } + }), + ast.Stmt(ast.ExprStmt{ + expr: ast.ComptimeExpr{ + expr: ast.IfExpr{ + cond: ast.Ident{ + name: 'macos' + } + stmts: [ + ast.Stmt(ast.ImportStmt{ + name: 'macmod' + }), + ] + } + } + }), + ] + } + windows_names := active_file_imports(file, [], 'windows').map(it.name) + linux_names := active_file_imports(file, [], 'linux').map(it.name) + macos_names := active_file_imports(file, [], 'macos').map(it.name) + + assert 'winmod' in windows_names + assert 'linmod' !in windows_names + assert 'macmod' !in windows_names + + assert 'linmod' in linux_names + assert 'winmod' !in linux_names + assert 'macmod' !in linux_names + + assert 'macmod' in macos_names + assert 'winmod' !in macos_names + assert 'linmod' !in macos_names +} + +fn test_header_cache_stamp_uses_target_os_preference() { + mut linux_prefs := pref.new_preferences() + linux_prefs.skip_builtin = true + linux_prefs.target_os = 'linux' + linux_builder := new_builder(&linux_prefs) + linux_stamp := linux_builder.header_stamp_for_modules([]) + + mut windows_prefs := pref.new_preferences() + windows_prefs.skip_builtin = true + windows_prefs.target_os = 'windows' + windows_builder := new_builder(&windows_prefs) + windows_stamp := windows_builder.header_stamp_for_modules([]) + + assert linux_stamp.contains('target_os=linux') + assert windows_stamp.contains('target_os=windows') + assert linux_stamp != windows_stamp +} + +struct WindowsX64ProbeResult { + undefined_symbols []string + built_functions []string + image []u8 +} + +fn test_windows_x64_empty_main_links_without_crt_or_darwin_symbols() { + res := build_windows_x64_probe('module main\nfn main() {}\n', false, true) + + assert 'calloc' !in res.undefined_symbols, res.undefined_symbols.str() + assert '__stdoutp' !in res.undefined_symbols, res.undefined_symbols.str() + assert '__stderrp' !in res.undefined_symbols, res.undefined_symbols.str() + assert '__stdinp' !in res.undefined_symbols, res.undefined_symbols.str() + assert '__error' !in res.undefined_symbols, res.undefined_symbols.str() + + linked := build_windows_x64_probe('module main\nfn main() {}\n', true, true) + assert linked.image.len > 0 + assert linked.image[0] == `M` + assert linked.image[1] == `Z` +} + +fn test_windows_x64_c_globals_do_not_use_darwin_symbols() { + source := 'module main +fn C.sink_ptr(voidptr) +fn C.sink_int(int) + +fn main() { + C.sink_ptr(C.stdout) + C.sink_ptr(C.stderr) + C.sink_ptr(C.stdin) + C.sink_int(C.errno) +} +' + res := build_windows_x64_probe(source, false, false) + + assert '__stdoutp' !in res.undefined_symbols + assert '__stderrp' !in res.undefined_symbols + assert '__stdinp' !in res.undefined_symbols + assert '__error' !in res.undefined_symbols + assert 'stdout' in res.undefined_symbols + assert 'stderr' in res.undefined_symbols + assert 'stdin' in res.undefined_symbols + assert 'errno' in res.undefined_symbols +} + +fn test_windows_x64_minimal_runtime_builds_markused_core_functions() { + res := build_windows_x64_probe("module main\nfn main() { println('x') }\n", false, false) + + assert 'builtin__println' in res.built_functions, res.built_functions.str() + assert 'stderr' !in res.undefined_symbols, res.undefined_symbols.str() + assert_no_windows_minimal_runtime_retention(res.built_functions) +} + +fn test_windows_x64_println_links_pe_image() { + linked := build_windows_x64_probe("module main\nfn main() { println('x') }\n", true, true) + + assert linked.image.len > 0 + assert linked.image[0] == `M` + assert linked.image[1] == `Z` + assert 'WriteConsoleW' in linked.undefined_symbols, linked.undefined_symbols.str() + assert 'MultiByteToWideChar' in linked.undefined_symbols, linked.undefined_symbols.str() + assert 'GetProcessHeap' in linked.undefined_symbols, linked.undefined_symbols.str() + assert 'HeapAlloc' in linked.undefined_symbols, linked.undefined_symbols.str() + assert 'HeapFree' in linked.undefined_symbols, linked.undefined_symbols.str() + assert 'WriteFile' in linked.undefined_symbols, linked.undefined_symbols.str() + assert_no_windows_minimal_crt_symbols(linked.undefined_symbols) + assert_no_windows_minimal_runtime_retention(linked.built_functions) +} + +fn test_windows_x64_minimal_runtime_preserves_imported_module_init() { + linked := build_windows_x64_probe_with_files({ + 'main.v': 'module main +import dep + +fn main() {} +' + 'dep/dep.v': 'module dep + +fn init() { + touch() +} + +fn touch() {} +' + }, true, true) + + assert 'dep__init' in linked.built_functions, linked.built_functions.str() + assert 'dep__touch' in linked.built_functions, linked.built_functions.str() + assert linked.image.len > 0 + assert linked.image[0] == `M` + assert linked.image[1] == `Z` +} + +fn test_windows_x64_minimal_runtime_preserves_conditional_imported_module_init() { + linked := build_windows_x64_probe_with_files({ + 'main.v': 'module main +$if windows { + import dep +} + +fn main() {} +' + 'dep/dep.v': 'module dep + +fn init() { + touch() +} + +fn touch() {} +' + }, true, true) + + assert 'dep__init' in linked.built_functions, linked.built_functions.str() + assert 'dep__touch' in linked.built_functions, linked.built_functions.str() + assert linked.image.len > 0 + assert linked.image[0] == `M` + assert linked.image[1] == `Z` +} + +fn test_windows_x64_minimal_runtime_preserves_dotted_import_module_init() { + linked := build_windows_x64_probe_with_files({ + 'main.v': 'module main +import fixture.inner + +fn main() {} +' + 'fixture/inner/inner.v': 'module inner + +const dotted_runtime_value = make_value() + +fn init() { + touch() +} + +fn touch() {} + +fn make_value() int { + return 7 +} +' + }, true, true) + + assert 'inner__init' in linked.built_functions, linked.built_functions.str() + assert 'inner__touch' in linked.built_functions, linked.built_functions.str() + assert 'inner____v_init_consts_inner' in linked.built_functions, linked.built_functions.str() + assert 'inner__make_value' in linked.built_functions, linked.built_functions.str() + assert linked.image.len > 0 + assert linked.image[0] == `M` + assert linked.image[1] == `Z` +} + +fn assert_no_windows_minimal_crt_symbols(undefined_symbols []string) { + for name in ['calloc', 'malloc', 'free', 'fwrite', 'fflush', 'write', 'stdout', 'stderr', + '_get_osfhandle'] { + assert name !in undefined_symbols, undefined_symbols.str() + } +} + +fn assert_no_windows_minimal_runtime_retention(built_functions []string) { + for name in built_functions { + assert !name.starts_with('os__'), built_functions.str() + assert !name.starts_with('time__'), built_functions.str() + assert !name.starts_with('io__'), built_functions.str() + assert !name.starts_with('dl__'), built_functions.str() + assert !name.starts_with('sha256__'), built_functions.str() + assert !name.starts_with('binary__'), built_functions.str() + if name.starts_with('__v_init_consts_') { + assert name == '__v_init_consts_main', built_functions.str() + } + assert !name.contains('____v_init_consts_'), built_functions.str() + } +} + +fn build_windows_x64_probe(source string, link bool, optimize bool) WindowsX64ProbeResult { + return build_windows_x64_probe_with_files({ + 'main.v': source + }, link, optimize) +} + +fn build_windows_x64_probe_with_files(sources map[string]string, link bool, optimize bool) WindowsX64ProbeResult { + tmp_dir := os.join_path(os.temp_dir(), + 'v2_builder_windows_x64_probe_${os.getpid()}_${sources.len}_${link}_${optimize}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + + main_path := os.join_path(tmp_dir, 'main.v') + obj_path := os.join_path(tmp_dir, 'main.obj') + exe_path := os.join_path(tmp_dir, 'main.exe') + for rel_path, source in sources { + path := os.join_path(tmp_dir, rel_path) + os.mkdir_all(os.dir(path)) or { panic(err) } + os.write_file(path, source) or { panic(err) } + } + + mut prefs := pref.new_preferences() + prefs.backend = .x64 + prefs.arch = .x64 + prefs.target_os = 'windows' + prefs.skip_type_check = true + prefs.no_parallel = true + prefs.no_parallel_transform = true + prefs.no_cache = true + + mut b := new_builder(&prefs) + b.user_files = [main_path] + b.files = b.parse_files([main_path]) + b.env = types.Environment.new() + + mut trans := transformer.Transformer.new_with_pref(b.files, b.env, b.pref) + trans.set_file_set(b.file_set) + b.files = trans.transform_files(b.files) + b.used_fn_keys = markused.mark_used_with_options(b.files, b.env, markused.MarkUsedOptions{ + minimal_runtime_roots: true + }) + assert b.used_fn_keys.len > 0 + + mut ssa_mod := ssa.Module.new('main') + mut ssa_builder := ssa.Builder.new_with_env(ssa_mod, b.env) + ssa_builder.guard_invalid_type_payloads = true + ssa_builder.target_os = 'windows' + ssa_builder.minimal_runtime_roots = true + ssa_builder.used_fn_keys = b.used_fn_keys.clone() + ssa_builder.build_all(b.files) + if optimize { + ssa_optimize.optimize(mut ssa_mod) + } + built_functions := ssa_mod.funcs.filter(it.blocks.len > 0).map(it.name) + + mut mir_mod := mir.lower_from_ssa(ssa_mod) + abi.lower_with_x64_abi(mut mir_mod, .x64, .windows) + insel.select(mut mir_mod, .x64) + + mut gen := x64.Gen.new_with_format_and_abi(&mir_mod, .coff, .windows) + gen.gen() + gen.write_file(obj_path) + mut image := []u8{} + if link { + gen.link_executable(exe_path) or { panic(err) } + image = os.read_bytes(exe_path) or { panic(err) } + } + return WindowsX64ProbeResult{ + undefined_symbols: coff_undefined_symbols(os.read_bytes(obj_path) or { panic(err) }) + built_functions: built_functions + image: image + } +} + +fn coff_undefined_symbols(data []u8) []string { + ptr_to_symbols := int(coff_u32(data, 8)) + number_of_symbols := int(coff_u32(data, 12)) + string_table_off := ptr_to_symbols + number_of_symbols * 18 + mut symbols := []string{} + mut i := 0 + for i < number_of_symbols { + off := ptr_to_symbols + i * 18 + section_number := i16(coff_u16(data, off + 12)) + aux_count := int(data[off + 17]) + if section_number == 0 { + symbols << coff_symbol_name(data, off, string_table_off) + } + i += 1 + aux_count + } + return symbols +} + +fn coff_symbol_name(data []u8, off int, string_table_off int) string { + if coff_u32(data, off) == 0 { + name_off := int(coff_u32(data, off + 4)) + return coff_string(data, string_table_off + name_off) + } + mut end := off + for end < off + 8 && data[end] != 0 { + end++ + } + return data[off..end].bytestr() +} + +fn coff_string(data []u8, off int) string { + mut end := off + for end < data.len && data[end] != 0 { + end++ + } + return data[off..end].bytestr() +} + +fn coff_u16(data []u8, off int) u16 { + return u16(data[off]) | (u16(data[off + 1]) << 8) +} + +fn coff_u32(data []u8, off int) u32 { + return u32(data[off]) | (u32(data[off + 1]) << 8) | (u32(data[off + 2]) << 16) | (u32(data[ + off + 3]) << 24) +} diff --git a/vlib/v2/gen/x64/abi.v b/vlib/v2/gen/x64/abi.v new file mode 100644 index 000000000..bac1dd7ad --- /dev/null +++ b/vlib/v2/gen/x64/abi.v @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +module x64 + +pub enum X64Abi { + sysv + windows +} + +const x64_no_arg_reg = -1 + +fn (abi X64Abi) int_arg_regs() []int { + return match abi { + .sysv { [int(rdi), int(rsi), int(rdx), int(rcx), int(r8), int(r9)] } + .windows { [int(rcx), int(rdx), int(r8), int(r9)] } + } +} + +fn (abi X64Abi) sret_arg_regs() []int { + return match abi { + .sysv { [int(rsi), int(rdx), int(rcx), int(r8), int(r9)] } + .windows { [int(rdx), int(r8), int(r9)] } + } +} + +fn (abi X64Abi) sret_reg() Reg { + return match abi { + .sysv { rdi } + .windows { rcx } + } +} + +fn (abi X64Abi) float_arg_regs() []int { + return match abi { + .sysv { [0, 1, 2, 3, 4, 5, 6, 7] } + .windows { [0, 1, 2, 3] } + } +} + +fn (abi X64Abi) uses_positional_arg_regs() bool { + return abi == .windows +} + +fn (abi X64Abi) int_arg_reg_at(index int) int { + regs := abi.int_arg_regs() + if index < 0 || index >= regs.len { + return x64_no_arg_reg + } + return regs[index] +} + +fn (abi X64Abi) float_arg_reg_at(index int) int { + regs := abi.float_arg_regs() + if index < 0 || index >= regs.len { + return x64_no_arg_reg + } + return regs[index] +} + +fn (abi X64Abi) int_arg_reg_for_position(arg_idx int) int { + return match abi { + .sysv { abi.int_arg_reg_at(arg_idx) } + .windows { abi.int_arg_reg_at(arg_idx) } + } +} + +fn (abi X64Abi) float_arg_reg_for_position(arg_idx int) int { + return match abi { + .sysv { abi.float_arg_reg_at(arg_idx) } + .windows { abi.float_arg_reg_at(arg_idx) } + } +} + +fn (abi X64Abi) shadow_space_size() int { + return match abi { + .sysv { 0 } + .windows { 32 } + } +} + +fn (abi X64Abi) stack_arg_offset(position int) int { + return match abi { + .sysv { 16 + position * 8 } + .windows { 48 + (position - 4) * 8 } + } +} + +fn (abi X64Abi) call_stack_arg_offset(position int) int { + return match abi { + .sysv { position * 8 } + .windows { abi.shadow_space_size() + (position - 4) * 8 } + } +} + +fn (abi X64Abi) call_frame_size(stack_slots int) int { + base := abi.shadow_space_size() + stack_slots * 8 + if base % 16 == 0 { + return base + } + return base + (16 - (base % 16)) +} diff --git a/vlib/v2/gen/x64/asm.v b/vlib/v2/gen/x64/asm.v index d309d77f0..dd8b113aa 100644 --- a/vlib/v2/gen/x64/asm.v +++ b/vlib/v2/gen/x64/asm.v @@ -762,6 +762,28 @@ fn asm_store_xmm_rbp_disp(mut g Gen, xmm int, disp int, size int) { } } +fn asm_store_xmm_mem_base_disp_size(mut g Gen, xmm int, base Reg, disp int, size int) { + base_hw := g.map_reg(int(base)) + if size == 4 { + g.emit(0xF3) + } else { + g.emit(0xF2) + } + mut rex := u8(0) + if xmm >= 8 { + rex |= 4 + } + if base_hw >= 8 { + rex |= 1 + } + if rex != 0 { + g.emit(0x40 | rex) + } + g.emit(0x0F) + g.emit(0x11) + asm_emit_modrm_base_disp(mut g, u8(xmm & 7), base_hw, disp) +} + fn asm_load_xmm_rbp_disp(mut g Gen, xmm int, disp int, size int) { if size == 4 { g.emit(0xF3) diff --git a/vlib/v2/gen/x64/coff.v b/vlib/v2/gen/x64/coff.v new file mode 100644 index 000000000..d4eb60a62 --- /dev/null +++ b/vlib/v2/gen/x64/coff.v @@ -0,0 +1,257 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +module x64 + +import os + +const coff_image_file_machine_amd64 = u16(0x8664) +const coff_image_sym_class_external = u8(2) +const coff_image_sym_class_static = u8(3) +const coff_image_sym_dtype_function = u16(0x20) + +const coff_image_rel_amd64_rel32 = u16(0x0004) + +const coff_image_scn_cnt_code = u32(0x00000020) +const coff_image_scn_cnt_initialized_data = u32(0x00000040) +const coff_image_scn_align_8bytes = u32(0x00400000) +const coff_image_scn_align_16bytes = u32(0x00500000) +const coff_image_scn_mem_execute = u32(0x20000000) +const coff_image_scn_mem_read = u32(0x40000000) +const coff_image_scn_mem_write = u32(0x80000000) + +pub struct CoffObject { +pub mut: + text_data []u8 + data_data []u8 + rodata []u8 + + symbols []CoffSymbol + str_table []u8 + sym_by_name map[string]int + + text_relocs []CoffRelocation +} + +struct CoffSymbol { +mut: + name string + name_off int + value u32 + section i16 + type_ u16 + storage_class u8 +} + +struct CoffRelocation { + offset u32 + sym_idx int + type_ u16 +} + +struct CoffSection { + name string + data []u8 + relocs []CoffRelocation + characteristics u32 +} + +pub fn CoffObject.new() &CoffObject { + return &CoffObject{} +} + +pub fn (mut c CoffObject) add_symbol(name string, value u64, is_func bool, section u8) int { + type_ := if is_func { coff_image_sym_dtype_function } else { u16(0) } + storage_class := coff_symbol_storage_class(name, section) + if i := c.sym_by_name[name] { + mut s := &c.symbols[i] + s.value = u32(value) + s.section = i16(section) + s.type_ = type_ + s.storage_class = storage_class + return i + } + + idx := c.symbols.len + c.symbols << CoffSymbol{ + name: name + name_off: c.add_string_if_needed(name) + value: u32(value) + section: i16(section) + type_: type_ + storage_class: storage_class + } + c.sym_by_name[name] = idx + return idx +} + +pub fn (mut c CoffObject) add_undefined(name string) int { + if i := c.sym_by_name[name] { + return i + } + + idx := c.symbols.len + c.symbols << CoffSymbol{ + name: name + name_off: c.add_string_if_needed(name) + value: 0 + section: 0 + type_: 0 + storage_class: coff_image_sym_class_external + } + c.sym_by_name[name] = idx + return idx +} + +fn coff_symbol_storage_class(name string, section u8) u8 { + if section != 0 && name.starts_with('L_') { + return coff_image_sym_class_static + } + return coff_image_sym_class_external +} + +pub fn (mut c CoffObject) add_text_reloc(offset int, sym_idx int, type_ u16) { + c.text_relocs << CoffRelocation{ + offset: u32(offset) + sym_idx: sym_idx + type_: type_ + } +} + +fn (mut c CoffObject) add_string_if_needed(s string) int { + if s.len <= 8 { + return 0 + } + off := 4 + c.str_table.len + c.str_table << s.bytes() + c.str_table << 0 + return off +} + +pub fn (mut c CoffObject) write(path string) { + sections := [ + CoffSection{ + name: '.text' + data: c.text_data + relocs: c.text_relocs + characteristics: coff_image_scn_cnt_code | coff_image_scn_mem_execute | coff_image_scn_mem_read | coff_image_scn_align_16bytes + }, + CoffSection{ + name: '.rdata' + data: c.rodata + characteristics: coff_image_scn_cnt_initialized_data | coff_image_scn_mem_read | coff_image_scn_align_8bytes + }, + CoffSection{ + name: '.data' + data: c.data_data + characteristics: coff_image_scn_cnt_initialized_data | coff_image_scn_mem_read | coff_image_scn_mem_write | coff_image_scn_align_8bytes + }, + ] + + header_size := 20 + section_header_size := 40 + reloc_entry_size := 10 + mut current_offset := header_size + (sections.len * section_header_size) + + mut raw_offsets := []int{len: sections.len} + for i, section in sections { + if section.data.len > 0 { + current_offset = align_int(current_offset, 4) + raw_offsets[i] = current_offset + current_offset += section.data.len + } + } + + mut reloc_offsets := []int{len: sections.len} + for i, section in sections { + if section.relocs.len > 0 { + current_offset = align_int(current_offset, 4) + reloc_offsets[i] = current_offset + current_offset += section.relocs.len * reloc_entry_size + } + } + + symbol_table_off := align_int(current_offset, 4) + string_table_size := 4 + c.str_table.len + + mut buf := []u8{} + write_u16_le(mut buf, coff_image_file_machine_amd64) + write_u16_le(mut buf, u16(sections.len)) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, u32(symbol_table_off)) + write_u32_le(mut buf, u32(c.symbols.len)) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + + for i, section in sections { + write_fixed_string(mut buf, section.name, 8) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, u32(section.data.len)) + write_u32_le(mut buf, u32(raw_offsets[i])) + write_u32_le(mut buf, u32(reloc_offsets[i])) + write_u32_le(mut buf, 0) + nrelocs := coff_relocation_count_for_header(section) or { panic(err) } + write_u16_le(mut buf, nrelocs) + write_u16_le(mut buf, 0) + write_u32_le(mut buf, section.characteristics) + } + + for i, section in sections { + if raw_offsets[i] == 0 { + continue + } + for buf.len < raw_offsets[i] { + buf << 0 + } + buf << section.data + } + + for i, section in sections { + if reloc_offsets[i] == 0 { + continue + } + for buf.len < reloc_offsets[i] { + buf << 0 + } + for reloc in section.relocs { + write_u32_le(mut buf, reloc.offset) + write_u32_le(mut buf, u32(reloc.sym_idx)) + write_u16_le(mut buf, reloc.type_) + } + } + + for buf.len < symbol_table_off { + buf << 0 + } + for sym in c.symbols { + c.write_symbol_name(mut buf, sym) + write_u32_le(mut buf, sym.value) + write_u16_le(mut buf, u16(sym.section)) + write_u16_le(mut buf, sym.type_) + buf << sym.storage_class + buf << 0 + } + + write_u32_le(mut buf, u32(string_table_size)) + buf << c.str_table + + os.write_file_array(path, buf) or { panic(err) } +} + +fn coff_relocation_count_for_header(section CoffSection) !u16 { + if section.relocs.len > 0xffff { + return error('COFF section ${section.name} has ${section.relocs.len} relocations; extended relocations are not supported') + } + return u16(section.relocs.len) +} + +fn (c CoffObject) write_symbol_name(mut buf []u8, sym CoffSymbol) { + if sym.name.len <= 8 { + write_fixed_string(mut buf, sym.name, 8) + return + } + write_u32_le(mut buf, 0) + write_u32_le(mut buf, u32(sym.name_off)) +} diff --git a/vlib/v2/gen/x64/object.v b/vlib/v2/gen/x64/object.v index 90d28cf1f..d7e01d8b0 100644 --- a/vlib/v2/gen/x64/object.v +++ b/vlib/v2/gen/x64/object.v @@ -9,6 +9,7 @@ import encoding.binary pub enum ObjectFormat { elf macho + coff } enum ObjectSection { @@ -33,6 +34,13 @@ fn (format ObjectFormat) section_index(section ObjectSection) u8 { .data { 3 } } } + .coff { + match section { + .text { 1 } + .rodata { 2 } + .data { 3 } + } + } } } @@ -47,6 +55,7 @@ fn (mut g Gen) text_len() int { return match g.obj_format { .elf { g.elf.text_data.len } .macho { g.macho.text_data.len } + .coff { g.coff.text_data.len } } } @@ -54,6 +63,7 @@ fn (mut g Gen) data_len() int { return match g.obj_format { .elf { g.elf.data_data.len } .macho { g.macho.data_data.len } + .coff { g.coff.data_data.len } } } @@ -61,6 +71,7 @@ fn (mut g Gen) rodata_len() int { return match g.obj_format { .elf { g.elf.rodata.len } .macho { g.macho.rodata.len } + .coff { g.coff.rodata.len } } } @@ -68,6 +79,7 @@ fn (mut g Gen) add_data_byte(b u8) { match g.obj_format { .elf { g.elf.data_data << b } .macho { g.macho.data_data << b } + .coff { g.coff.data_data << b } } } @@ -75,6 +87,7 @@ fn (mut g Gen) add_data(bytes []u8) { match g.obj_format { .elf { g.elf.data_data << bytes } .macho { g.macho.data_data << bytes } + .coff { g.coff.data_data << bytes } } } @@ -82,6 +95,7 @@ fn (mut g Gen) add_rodata_byte(b u8) { match g.obj_format { .elf { g.elf.rodata << b } .macho { g.macho.rodata << b } + .coff { g.coff.rodata << b } } } @@ -89,6 +103,7 @@ fn (mut g Gen) add_rodata(bytes []u8) { match g.obj_format { .elf { g.elf.rodata << bytes } .macho { g.macho.rodata << bytes } + .coff { g.coff.rodata << bytes } } } @@ -98,6 +113,7 @@ fn (mut g Gen) add_symbol(name string, value u64, is_func bool, section ObjectSe return match g.obj_format { .elf { g.elf.add_symbol(sym_name, value, is_func, u16(sect)) } .macho { g.macho.add_symbol(sym_name, value, !sym_name.starts_with('L_'), sect) } + .coff { g.coff.add_symbol(sym_name, value, is_func, sect) } } } @@ -106,6 +122,7 @@ fn (mut g Gen) add_undefined(name string) int { return match g.obj_format { .elf { g.elf.add_undefined(sym_name) } .macho { g.macho.add_undefined(sym_name) } + .coff { g.coff.add_undefined(sym_name) } } } @@ -114,6 +131,7 @@ fn (mut g Gen) add_call_reloc(sym_idx int) { match g.obj_format { .elf { g.elf.add_text_reloc(u64(offset), sym_idx, r_x86_64_plt32, -4) } .macho { g.macho.add_reloc(offset, sym_idx, x86_64_reloc_branch, true, 2) } + .coff { g.coff.add_text_reloc(offset, sym_idx, coff_image_rel_amd64_rel32) } } } @@ -122,6 +140,7 @@ fn (mut g Gen) add_rip_reloc(sym_idx int) { match g.obj_format { .elf { g.elf.add_text_reloc(u64(offset), sym_idx, r_x86_64_pc32, -4) } .macho { g.macho.add_reloc(offset, sym_idx, x86_64_reloc_signed, true, 2) } + .coff { g.coff.add_text_reloc(offset, sym_idx, coff_image_rel_amd64_rel32) } } } @@ -129,6 +148,7 @@ fn (mut g Gen) emit(b u8) { match g.obj_format { .elf { g.elf.text_data << b } .macho { g.macho.text_data << b } + .coff { g.coff.text_data << b } } } @@ -148,6 +168,7 @@ fn (mut g Gen) write_u32(off int, v u32) { match g.obj_format { .elf { binary.little_endian_put_u32(mut g.elf.text_data[off..off + 4], v) } .macho { binary.little_endian_put_u32(mut g.macho.text_data[off..off + 4], v) } + .coff { binary.little_endian_put_u32(mut g.coff.text_data[off..off + 4], v) } } } @@ -155,5 +176,21 @@ pub fn (mut g Gen) write_file(path string) { match g.obj_format { .elf { g.elf.write(path) } .macho { g.macho.write(path) } + .coff { g.coff.write(path) } + } +} + +pub fn (mut g Gen) link_executable(path string) ! { + match g.obj_format { + .coff { + if g.abi != .windows { + return error('x64 PE linking requires Windows ABI code generation') + } + mut linker := PeLinker.new(g.coff) + linker.write(path)! + } + else { + return error('x64 built-in executable linking is only implemented for COFF') + } } } diff --git a/vlib/v2/gen/x64/pe.v b/vlib/v2/gen/x64/pe.v new file mode 100644 index 000000000..22c41dbd0 --- /dev/null +++ b/vlib/v2/gen/x64/pe.v @@ -0,0 +1,474 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +module x64 + +import os + +const pe_dos_stub_size = 0x80 +const pe_signature = u32(0x00004550) +const pe_optional_header64_magic = u16(0x20b) +const pe_file_alignment = 0x200 +const pe_section_alignment = 0x1000 +const pe_image_base = u64(0x140000000) +const pe_size_of_optional_header64 = u16(0xf0) +const pe_number_of_rva_and_sizes = 16 + +const pe_image_file_relocs_stripped = u16(0x0001) +const pe_image_file_executable_image = u16(0x0002) +const pe_image_file_large_address_aware = u16(0x0020) +const pe_image_subsystem_windows_cui = u16(3) +const pe_dll_characteristics_nx_compat = u16(0x0100) + +const pe_image_scn_cnt_code = u32(0x00000020) +const pe_image_scn_cnt_initialized_data = u32(0x00000040) +const pe_image_scn_mem_execute = u32(0x20000000) +const pe_image_scn_mem_read = u32(0x40000000) +const pe_image_scn_mem_write = u32(0x80000000) + +const pe_import_directory_index = 1 +const pe_exception_directory_index = 3 +const pe_base_reloc_directory_index = 5 +const pe_iat_directory_index = 12 + +const pe_kernel32_imports = ['ExitProcess', 'GetStdHandle', 'GetConsoleMode', 'MultiByteToWideChar', + 'WriteConsoleW', 'WriteFile', 'GetProcessHeap', 'HeapAlloc', 'HeapFree'] + +struct PeImport { + dll string + name string +} + +struct PeSection { +mut: + name string + data []u8 + virtual_address u32 + virtual_size u32 + raw_pointer u32 + raw_size u32 + characteristics u32 +} + +struct PeIdata { + data []u8 + import_rva u32 + import_size u32 + iat_rva u32 + iat_size u32 +} + +pub struct PeLinker { + coff &CoffObject +mut: + imports []PeImport +} + +pub fn PeLinker.new(coff &CoffObject) &PeLinker { + mut linker := unsafe { + &PeLinker{ + coff: coff + } + } + for name in pe_kernel32_imports { + linker.imports << PeImport{ + dll: 'kernel32.dll' + name: name + } + } + return linker +} + +pub fn (mut l PeLinker) write(path string) ! { + image := l.image()! + os.write_file_array(path, image)! +} + +pub fn (mut l PeLinker) image() ![]u8 { + mut text := l.build_text_section()! + text_virtual_size := text.len + rdata_virtual_size := l.coff.rodata.len + data_virtual_size := l.coff.data_data.len + + header_size := pe_headers_size(4) + text_rva := u32(pe_section_alignment) + rdata_rva := pe_next_section_rva(text_rva, text_virtual_size) + data_rva := pe_next_section_rva(rdata_rva, rdata_virtual_size) + idata_rva := pe_next_section_rva(data_rva, data_virtual_size) + + idata := l.build_idata(idata_rva) + mut raw_pointer := header_size + + text_section, next_raw := pe_make_section('.text', text, text_rva, raw_pointer, + pe_image_scn_cnt_code | pe_image_scn_mem_execute | pe_image_scn_mem_read) + raw_pointer = next_raw + rdata_section, next_raw2 := pe_make_section('.rdata', l.coff.rodata, rdata_rva, raw_pointer, + pe_image_scn_cnt_initialized_data | pe_image_scn_mem_read) + raw_pointer = next_raw2 + data_section, next_raw3 := pe_make_section('.data', l.coff.data_data, data_rva, raw_pointer, + pe_image_scn_cnt_initialized_data | pe_image_scn_mem_read | pe_image_scn_mem_write) + raw_pointer = next_raw3 + idata_section, next_raw4 := pe_make_section('.idata', idata.data, idata_rva, raw_pointer, + pe_image_scn_cnt_initialized_data | pe_image_scn_mem_read | pe_image_scn_mem_write) + raw_pointer = next_raw4 + sections := [text_section, rdata_section, data_section, idata_section] + + import_thunks := l.import_thunk_rvas(text_section.virtual_address) + l.apply_text_relocations(mut text, text_section.virtual_address, rdata_section.virtual_address, + data_section.virtual_address, import_thunks)! + + mut patched_sections := sections.clone() + patched_sections[0].data = text + + size_of_image := pe_size_of_image(patched_sections) + size_of_code := text_section.raw_size + size_of_initialized_data := rdata_section.raw_size + data_section.raw_size + + idata_section.raw_size + + mut buf := []u8{cap: int(raw_pointer)} + write_pe_dos_stub(mut buf) + for buf.len < pe_dos_stub_size { + buf << 0 + } + + write_u32_le(mut buf, pe_signature) + write_u16_le(mut buf, coff_image_file_machine_amd64) + write_u16_le(mut buf, u16(patched_sections.len)) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u16_le(mut buf, pe_size_of_optional_header64) + // No .reloc section is emitted yet, so mark the image fixed-base. + write_u16_le(mut buf, + pe_image_file_relocs_stripped | pe_image_file_executable_image | pe_image_file_large_address_aware) + + l.write_optional_header(mut buf, size_of_code, size_of_initialized_data, size_of_image, + header_size, idata, text_section.virtual_address) + + for section in patched_sections { + write_pe_section_header(mut buf, section) + } + + for buf.len < header_size { + buf << 0 + } + for section in patched_sections { + if section.raw_size == 0 { + continue + } + for buf.len < int(section.raw_pointer) { + buf << 0 + } + buf << section.data + for buf.len < int(section.raw_pointer + section.raw_size) { + buf << 0 + } + } + + return buf +} + +fn (l PeLinker) build_text_section() ![]u8 { + main_rva := l.defined_symbol_offset('main') or { + return error('PE linker requires a defined main symbol') + } + mut text := []u8{} + vinit_offset := l.defined_symbol_offset('_vinit') or { u32(0xffff_ffff) } + text << [u8(0x48), 0x83, 0xec, 0x28] // sub rsp, 40 + if vinit_offset != 0xffff_ffff { + pe_emit_call_placeholder(mut text) + } + pe_emit_call_placeholder(mut text) + text << [u8(0x31), 0xc9] // xor ecx, ecx + pe_emit_call_placeholder(mut text) + text << 0xcc + + entry_stub_len := text.len + text << l.coff.text_data + for _ in l.imports { + text << [u8(0xff), 0x25, 0, 0, 0, 0] // jmp qword ptr [rip + disp32] + } + + mut cursor := 4 + if vinit_offset != 0xffff_ffff { + pe_patch_rel32(mut text, cursor + 1, u32(entry_stub_len) + vinit_offset, 0) + cursor += 5 + } + pe_patch_rel32(mut text, cursor + 1, u32(entry_stub_len) + main_rva, 0) + cursor += 5 + cursor += 2 + exit_thunk_off := u32(entry_stub_len + l.coff.text_data.len) + pe_patch_rel32(mut text, cursor + 1, exit_thunk_off, 0) + + return text +} + +fn (l PeLinker) build_idata(idata_rva u32) PeIdata { + descriptor_size := 20 + descriptor_count := 2 // kernel32 + null terminator + ilt_off := descriptor_size * descriptor_count + ilt_size := (l.imports.len + 1) * 8 + iat_off := ilt_off + ilt_size + iat_size := (l.imports.len + 1) * 8 + hint_name_off := iat_off + iat_size + + mut data := []u8{len: hint_name_off} + mut hint_name_rvas := []u32{cap: l.imports.len} + for imp in l.imports { + hint_name_rvas << idata_rva + u32(data.len) + write_u16_le(mut data, 0) + data << imp.name.bytes() + data << 0 + if data.len % 2 != 0 { + data << 0 + } + } + dll_name_rva := idata_rva + u32(data.len) + data << 'kernel32.dll'.bytes() + data << 0 + + pe_put_u32_le(mut data, 0, idata_rva + u32(ilt_off)) + pe_put_u32_le(mut data, 12, dll_name_rva) + pe_put_u32_le(mut data, 16, idata_rva + u32(iat_off)) + for i, rva in hint_name_rvas { + pe_put_u64_le(mut data, ilt_off + i * 8, u64(rva)) + pe_put_u64_le(mut data, iat_off + i * 8, u64(rva)) + } + + return PeIdata{ + data: data + import_rva: idata_rva + import_size: u32(descriptor_size * descriptor_count) + iat_rva: idata_rva + u32(iat_off) + iat_size: u32(iat_size) + } +} + +fn (l PeLinker) apply_text_relocations(mut text []u8, text_rva u32, rdata_rva u32, data_rva u32, import_thunks map[string]u32) ! { + entry_stub_len := l.entry_stub_size() + for reloc in l.coff.text_relocs { + if reloc.type_ != coff_image_rel_amd64_rel32 { + return error('PE linker does not support COFF relocation type 0x${reloc.type_:04x}') + } + if reloc.sym_idx < 0 || reloc.sym_idx >= l.coff.symbols.len { + return error('PE linker relocation references invalid symbol index ${reloc.sym_idx}') + } + sym := l.coff.symbols[reloc.sym_idx] + target_rva := match sym.section { + 1 { + text_rva + u32(entry_stub_len) + sym.value + } + 2 { + rdata_rva + sym.value + } + 3 { + data_rva + sym.value + } + 0 { + import_thunks[sym.name] or { + return error('PE linker cannot resolve external symbol `${sym.name}` yet') + } + } + else { + return error('PE linker cannot resolve symbol `${sym.name}` in COFF section ${sym.section}') + } + } + + field_off := entry_stub_len + int(reloc.offset) + old_disp := pe_read_i32_le(text, field_off) + field_rva := text_rva + u32(field_off) + new_disp := old_disp + i32(int(target_rva) - (int(field_rva) + 4)) + pe_put_u32_le(mut text, field_off, u32(new_disp)) + } + + thunk_base := entry_stub_len + l.coff.text_data.len + for i, imp in l.imports { + thunk_off := thunk_base + i * 6 + iat_entry_rva := l.iat_entry_rva(imp.name) + thunk_rva := text_rva + u32(thunk_off) + disp := i32(int(iat_entry_rva) - (int(thunk_rva) + 6)) + pe_put_u32_le(mut text, thunk_off + 2, u32(disp)) + } +} + +fn (l PeLinker) import_thunk_rvas(text_rva u32) map[string]u32 { + entry_stub_len := l.entry_stub_size() + thunk_base := entry_stub_len + l.coff.text_data.len + mut out := map[string]u32{} + for i, imp in l.imports { + out[imp.name] = text_rva + u32(thunk_base + i * 6) + } + return out +} + +fn (l PeLinker) iat_entry_rva(name string) u32 { + for i, imp in l.imports { + if imp.name == name { + return l.idata_iat_rva() + u32(i * 8) + } + } + return 0 +} + +fn (l PeLinker) idata_iat_rva() u32 { + text_size := l.entry_stub_size() + l.coff.text_data.len + l.imports.len * 6 + rdata_rva := pe_next_section_rva(u32(pe_section_alignment), text_size) + data_rva := pe_next_section_rva(rdata_rva, l.coff.rodata.len) + idata_rva := pe_next_section_rva(data_rva, l.coff.data_data.len) + ilt_off := 20 * 2 + ilt_size := (l.imports.len + 1) * 8 + iat_off := ilt_off + ilt_size + return idata_rva + u32(iat_off) +} + +fn (l PeLinker) entry_stub_size() int { + vinit := l.defined_symbol_offset('_vinit') or { u32(0xffff_ffff) } + return if vinit == 0xffff_ffff { 17 } else { 22 } +} + +fn (l PeLinker) defined_symbol_offset(name string) ?u32 { + for sym in l.coff.symbols { + if sym.name == name && sym.section == 1 { + return sym.value + } + } + return none +} + +fn (mut l PeLinker) write_optional_header(mut buf []u8, size_of_code u32, size_of_initialized_data u32, size_of_image u32, size_of_headers int, idata PeIdata, entry_rva u32) { + write_u16_le(mut buf, pe_optional_header64_magic) + buf << 0 + buf << 1 + write_u32_le(mut buf, size_of_code) + write_u32_le(mut buf, size_of_initialized_data) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, entry_rva) + write_u32_le(mut buf, entry_rva) + write_u64_le(mut buf, pe_image_base) + write_u32_le(mut buf, pe_section_alignment) + write_u32_le(mut buf, pe_file_alignment) + write_u16_le(mut buf, 6) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 6) + write_u16_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, size_of_image) + write_u32_le(mut buf, u32(size_of_headers)) + write_u32_le(mut buf, 0) + write_u16_le(mut buf, pe_image_subsystem_windows_cui) + write_u16_le(mut buf, pe_dll_characteristics_nx_compat) + write_u64_le(mut buf, 0x100000) + write_u64_le(mut buf, 0x1000) + write_u64_le(mut buf, 0x100000) + write_u64_le(mut buf, 0x1000) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, pe_number_of_rva_and_sizes) + + for i in 0 .. pe_number_of_rva_and_sizes { + if i == pe_import_directory_index { + write_u32_le(mut buf, idata.import_rva) + write_u32_le(mut buf, idata.import_size) + } else if i == pe_exception_directory_index || i == pe_base_reloc_directory_index { + // Unwind and base relocation directories are not emitted yet. + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + } else if i == pe_iat_directory_index { + write_u32_le(mut buf, idata.iat_rva) + write_u32_le(mut buf, idata.iat_size) + } else { + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + } + } +} + +fn write_pe_dos_stub(mut buf []u8) { + buf << [u8(`M`), `Z`] + for buf.len < 0x3c { + buf << 0 + } + write_u32_le(mut buf, pe_dos_stub_size) +} + +fn write_pe_section_header(mut buf []u8, section PeSection) { + write_fixed_string(mut buf, section.name, 8) + write_u32_le(mut buf, section.virtual_size) + write_u32_le(mut buf, section.virtual_address) + write_u32_le(mut buf, section.raw_size) + write_u32_le(mut buf, section.raw_pointer) + write_u32_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + write_u32_le(mut buf, section.characteristics) +} + +fn pe_make_section(name string, data []u8, virtual_address u32, raw_pointer int, characteristics u32) (PeSection, int) { + raw_size := if data.len == 0 { 0 } else { align_int(data.len, pe_file_alignment) } + raw_ptr := if raw_size == 0 { 0 } else { raw_pointer } + mut next_raw_pointer := raw_pointer + if raw_size > 0 { + next_raw_pointer += raw_size + } + return PeSection{ + name: name + data: data.clone() + virtual_address: virtual_address + virtual_size: u32(data.len) + raw_pointer: u32(raw_ptr) + raw_size: u32(raw_size) + characteristics: characteristics + }, next_raw_pointer +} + +fn pe_headers_size(section_count int) int { + return align_int(pe_dos_stub_size + 4 + 20 + int(pe_size_of_optional_header64) + + section_count * 40, pe_file_alignment) +} + +fn pe_size_of_image(sections []PeSection) u32 { + mut end := 0 + for section in sections { + virtual_size := if section.virtual_size == 0 { 1 } else { int(section.virtual_size) } + section_end := int(section.virtual_address) + align_int(virtual_size, pe_section_alignment) + if section_end > end { + end = section_end + } + } + return u32(align_int(end, pe_section_alignment)) +} + +fn pe_next_section_rva(rva u32, size int) u32 { + virtual_size := if size == 0 { 1 } else { size } + return u32(align_int(int(rva) + virtual_size, pe_section_alignment)) +} + +fn pe_emit_call_placeholder(mut text []u8) { + text << [u8(0xe8), 0, 0, 0, 0] +} + +fn pe_patch_rel32(mut text []u8, field_off int, target_off u32, text_rva u32) { + field_rva := text_rva + u32(field_off) + disp := i32(int(target_off) - (int(field_rva) + 4)) + pe_put_u32_le(mut text, field_off, u32(disp)) +} + +fn pe_read_i32_le(data []u8, off int) i32 { + return i32(u32(data[off]) | (u32(data[off + 1]) << 8) | (u32(data[off + 2]) << 16) | (u32(data[ + off + 3]) << 24)) +} + +fn pe_put_u32_le(mut data []u8, off int, v u32) { + data[off] = u8(v) + data[off + 1] = u8(v >> 8) + data[off + 2] = u8(v >> 16) + data[off + 3] = u8(v >> 24) +} + +fn pe_put_u64_le(mut data []u8, off int, v u64) { + pe_put_u32_le(mut data, off, u32(v)) + pe_put_u32_le(mut data, off + 4, u32(v >> 32)) +} diff --git a/vlib/v2/gen/x64/x64.v b/vlib/v2/gen/x64/x64.v index be81c17e7..545ff47e9 100644 --- a/vlib/v2/gen/x64/x64.v +++ b/vlib/v2/gen/x64/x64.v @@ -14,7 +14,9 @@ pub struct Gen { mut: elf &ElfObject macho &MachOObject + coff &CoffObject obj_format ObjectFormat + abi X64Abi stack_map map[int]int alloca_offsets map[int]int @@ -46,16 +48,24 @@ pub fn Gen.new(mod &mir.Module) &Gen { mod: mod elf: ElfObject.new() macho: MachOObject.new() + coff: CoffObject.new() obj_format: .elf + abi: .sysv } } pub fn Gen.new_with_format(mod &mir.Module, obj_format ObjectFormat) &Gen { + return Gen.new_with_format_and_abi(mod, obj_format, .sysv) +} + +pub fn Gen.new_with_format_and_abi(mod &mir.Module, obj_format ObjectFormat, abi X64Abi) &Gen { return &Gen{ mod: mod elf: ElfObject.new() macho: MachOObject.new() + coff: CoffObject.new() obj_format: obj_format + abi: abi } } @@ -121,7 +131,7 @@ fn (mut g Gen) gen_func(func mir.Function) { // Start after callee-saved pushes so locals do not overlap their rbp slots. mut slot_offset := g.used_regs.len * 8 - // Hidden sret pointer slot (SysV: incoming in RDI) + // Hidden sret pointer slot (SysV: incoming in RDI, Windows: incoming in RCX) if func.abi_ret_indirect { off, next_offset := reserve_stack_bytes(slot_offset, 8, 1) g.sret_save_offset = off @@ -216,25 +226,28 @@ fn (mut g Gen) gen_func(func mir.Function) { } } - // Move Params (ABI: RDI, RSI, RDX, RCX, R8, R9) - // For sret functions, hidden pointer consumes RDI. - abi_regs := [7, 6, 2, 1, 8, 9] + abi_regs := g.abi.int_arg_regs() arg_reg_base := if func.abi_ret_indirect { 1 } else { 0 } mut reg_arg_idx := arg_reg_base mut sse_arg_idx := 0 + float_arg_regs := g.abi.float_arg_regs() if func.abi_ret_indirect && g.sret_save_offset != 0 { - asm_store_rbp_disp_reg(mut g, g.sret_save_offset, rdi) + asm_store_rbp_disp_reg(mut g, g.sret_save_offset, g.abi.sret_reg()) } mut stack_param_offset := 16 for i, pid in func.params { is_indirect_param := i < func.abi_param_class.len && func.abi_param_class[i] == .indirect param_size := g.type_size(g.mod.values[pid].typ) + if g.abi == .windows { + g.move_windows_param(pid, i + arg_reg_base, is_indirect_param, param_size) + continue + } if g.value_is_float_type(pid) { g.ensure_float_abi_scalar(pid, 'parameter') - if sse_arg_idx >= 8 { + if sse_arg_idx >= float_arg_regs.len { g.unsupported_float_abi('stack parameter', pid) } - asm_store_xmm_rbp_disp(mut g, sse_arg_idx, g.stack_map[pid], param_size) + asm_store_xmm_rbp_disp(mut g, float_arg_regs[sse_arg_idx], g.stack_map[pid], param_size) sse_arg_idx++ continue } @@ -244,7 +257,7 @@ fn (mut g Gen) gen_func(func mir.Function) { } else { 1 } - if reg_arg_idx + param_chunks <= 6 { + if reg_arg_idx + param_chunks <= abi_regs.len { src := abi_regs[reg_arg_idx] if is_indirect_param { g.copy_indirect_param_from_reg(pid, src) @@ -321,6 +334,50 @@ fn reserve_stack_bytes(slot_offset int, size int, align int) (int, int) { return -next_offset, next_offset } +fn (mut g Gen) move_windows_param(pid int, position int, is_indirect_param bool, param_size int) { + if g.value_is_float_type(pid) { + g.ensure_float_abi_scalar(pid, 'parameter') + if position < 4 { + asm_store_xmm_rbp_disp(mut g, g.abi.float_arg_reg_for_position(position), + g.stack_map[pid], param_size) + } else { + asm_load_xmm_mem_base_disp_size(mut g, 0, rbp, g.abi.stack_arg_offset(position), + param_size) + asm_store_xmm_rbp_disp(mut g, 0, g.stack_map[pid], param_size) + } + return + } + param_is_indirect := g.windows_value_passed_indirect(pid, is_indirect_param, param_size) + g.ensure_windows_scalar_or_indirect_arg(pid, param_is_indirect, param_size) + if position < 4 { + src := g.abi.int_arg_reg_for_position(position) + if param_is_indirect { + g.copy_indirect_param_from_reg(pid, src) + } else if g.value_needs_raw_abi_reg_bytes(pid, param_size) { + g.store_reg_to_rbp_exact(Reg(src), g.stack_map[pid], param_size) + } else if reg := g.reg_map[pid] { + asm_mov_reg_reg(mut g, Reg(reg), Reg(src)) + } else { + asm_store_rbp_disp_reg(mut g, g.stack_map[pid], Reg(src)) + } + return + } + stack_param_offset := g.abi.stack_arg_offset(position) + if param_is_indirect { + asm_load_reg_rbp_disp(mut g, rax, stack_param_offset) + g.copy_indirect_param_from_reg(pid, int(rax)) + } else if g.value_needs_raw_abi_reg_bytes(pid, param_size) { + asm_load_reg_rbp_disp(mut g, rax, stack_param_offset) + g.store_reg_to_rbp_exact(rax, g.stack_map[pid], param_size) + } else if reg := g.reg_map[pid] { + asm_load_reg_rbp_disp(mut g, rax, stack_param_offset) + asm_mov_reg_reg(mut g, Reg(reg), rax) + } else { + asm_load_reg_rbp_disp(mut g, rax, stack_param_offset) + asm_store_rbp_disp_reg(mut g, g.stack_map[pid], rax) + } +} + fn (mut g Gen) gen_instr(val_id int) { instr := g.mod.instrs[g.mod.values[val_id].index] op := g.selected_opcode(instr) @@ -444,13 +501,20 @@ fn (mut g Gen) gen_instr(val_id int) { } .heap_alloc { alloc_size := g.heap_alloc_size(val_id) - asm_mov_reg_imm32(mut g, rdi, 1) - asm_mov_reg_imm64(mut g, rsi, u64(alloc_size)) - asm_xor_eax_eax(mut g) + cleanup := g.emit_windows_call_frame(0) + if g.abi == .windows { + asm_mov_reg_imm32(mut g, rcx, 1) + asm_mov_reg_imm64(mut g, rdx, u64(alloc_size)) + } else { + asm_mov_reg_imm32(mut g, rdi, 1) + asm_mov_reg_imm64(mut g, rsi, u64(alloc_size)) + asm_xor_eax_eax(mut g) + } asm_call_rel32(mut g) sym_idx := g.add_undefined('calloc') g.add_call_reloc(sym_idx) g.emit_u32(0) + g.cleanup_windows_call_frame(cleanup) g.store_reg_to_val(0, val_id) } .get_element_ptr { @@ -478,12 +542,12 @@ fn (mut g Gen) gen_instr(val_id int) { g.store_reg_to_val(0, val_id) } .call { - abi_regs := [7, 6, 2, 1, 8, 9] + abi_regs := g.abi.int_arg_regs() num_args := instr.operands.len - 1 - stack_args := g.call_stack_arg_mask(instr, abi_regs.len) + stack_args := g.call_stack_arg_mask(instr, abi_regs.len, 0) stack_slots := g.call_stack_slots(instr, stack_args) - - if stack_slots > 0 { + cleanup := g.prepare_call_stack_args(instr, stack_args, stack_slots, 0) + if g.abi == .sysv && stack_slots > 0 { if stack_slots % 2 == 1 { asm_push(mut g, rax) } @@ -495,7 +559,7 @@ fn (mut g Gen) gen_instr(val_id int) { } // Stack arguments were pushed above; this pass only loads register arguments. - sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args) + sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args, 0) fn_val := g.mod.values[instr.operands[0]] // AL carries the number of SSE argument registers for variadic calls. @@ -513,12 +577,14 @@ fn (mut g Gen) gen_instr(val_id int) { } // Clean up stack arguments - if stack_slots > 0 { - cleanup := (stack_slots + (stack_slots % 2)) * 8 - if cleanup <= 127 { - asm_add_rsp_imm8(mut g, u8(cleanup)) + if g.abi == .windows { + g.cleanup_windows_call_frame(cleanup) + } else if stack_slots > 0 { + sysv_cleanup := (stack_slots + (stack_slots % 2)) * 8 + if sysv_cleanup <= 127 { + asm_add_rsp_imm8(mut g, u8(sysv_cleanup)) } else { - asm_add_rsp_imm32(mut g, u32(cleanup)) + asm_add_rsp_imm32(mut g, u32(sysv_cleanup)) } } @@ -527,16 +593,16 @@ fn (mut g Gen) gen_instr(val_id int) { } } .call_sret { - // SysV x86_64: hidden sret pointer in RDI, then user args in RSI,RDX,RCX,R8,R9. - abi_regs := [6, 2, 1, 8, 9] + abi_regs := g.abi.sret_arg_regs() num_args := instr.operands.len - 1 - stack_args := g.call_stack_arg_mask(instr, abi_regs.len) + arg_position_base := if g.abi == .windows { 1 } else { 0 } + stack_args := g.call_stack_arg_mask(instr, abi_regs.len, arg_position_base) stack_slots := g.call_stack_slots(instr, stack_args) - // Load destination pointer to RDI. - g.load_address_of_val_to_reg(7, val_id) + g.load_address_of_val_to_reg(int(g.abi.sret_reg()), val_id) - if stack_slots > 0 { + cleanup := g.prepare_call_stack_args(instr, stack_args, stack_slots, arg_position_base) + if g.abi == .sysv && stack_slots > 0 { if stack_slots % 2 == 1 { asm_push(mut g, rax) } @@ -548,7 +614,7 @@ fn (mut g Gen) gen_instr(val_id int) { } // Stack arguments were pushed above; this pass only loads register arguments. - sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args) + sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args, arg_position_base) fn_val := g.mod.values[instr.operands[0]] @@ -566,24 +632,26 @@ fn (mut g Gen) gen_instr(val_id int) { } // Clean up stack arguments - if stack_slots > 0 { - cleanup := (stack_slots + (stack_slots % 2)) * 8 - if cleanup <= 127 { - asm_add_rsp_imm8(mut g, u8(cleanup)) + if g.abi == .windows { + g.cleanup_windows_call_frame(cleanup) + } else if stack_slots > 0 { + sysv_cleanup := (stack_slots + (stack_slots % 2)) * 8 + if sysv_cleanup <= 127 { + asm_add_rsp_imm8(mut g, u8(sysv_cleanup)) } else { - asm_add_rsp_imm32(mut g, u32(cleanup)) + asm_add_rsp_imm32(mut g, u32(sysv_cleanup)) } } } .call_indirect { // Indirect call through function pointer // operands[0] is the function pointer, rest are arguments - abi_regs := [7, 6, 2, 1, 8, 9] + abi_regs := g.abi.int_arg_regs() num_args := instr.operands.len - 1 - stack_args := g.call_stack_arg_mask(instr, abi_regs.len) + stack_args := g.call_stack_arg_mask(instr, abi_regs.len, 0) stack_slots := g.call_stack_slots(instr, stack_args) - - if stack_slots > 0 { + cleanup := g.prepare_call_stack_args(instr, stack_args, stack_slots, 0) + if g.abi == .sysv && stack_slots > 0 { if stack_slots % 2 == 1 { asm_push(mut g, rax) } @@ -595,7 +663,7 @@ fn (mut g Gen) gen_instr(val_id int) { } // Stack arguments were pushed above; this pass only loads register arguments. - sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args) + sse_arg_idx := g.load_call_register_args(instr, abi_regs, stack_args, 0) // Load function pointer to r10 (caller-saved, not used for args) g.load_val_to_reg(10, instr.operands[0]) @@ -607,12 +675,14 @@ fn (mut g Gen) gen_instr(val_id int) { asm_call_r10(mut g) // Clean up stack arguments - if stack_slots > 0 { - cleanup := (stack_slots + (stack_slots % 2)) * 8 - if cleanup <= 127 { - asm_add_rsp_imm8(mut g, u8(cleanup)) + if g.abi == .windows { + g.cleanup_windows_call_frame(cleanup) + } else if stack_slots > 0 { + sysv_cleanup := (stack_slots + (stack_slots % 2)) * 8 + if sysv_cleanup <= 127 { + asm_add_rsp_imm8(mut g, u8(sysv_cleanup)) } else { - asm_add_rsp_imm32(mut g, u32(cleanup)) + asm_add_rsp_imm32(mut g, u32(sysv_cleanup)) } } @@ -623,18 +693,17 @@ fn (mut g Gen) gen_instr(val_id int) { .ret { if g.cur_func_abi_ret_indirect { if g.sret_save_offset != 0 { - asm_load_reg_rbp_disp(mut g, rdi, g.sret_save_offset) + asm_load_reg_rbp_disp(mut g, g.abi.sret_reg(), g.sret_save_offset) } if instr.operands.len > 0 { ret_val_id := instr.operands[0] ret_size := g.type_size(g.cur_func_ret_type) if ret_size > 0 { g.load_struct_src_address_to_reg(int(r10), ret_val_id, g.cur_func_ret_type) - g.copy_memory(int(rdi), 0, int(r10), 0, ret_size) + g.copy_memory(int(g.abi.sret_reg()), 0, int(r10), 0, ret_size) } } - // SysV returns sret pointer in RAX. - asm_mov_reg_reg(mut g, rax, rdi) + asm_mov_reg_reg(mut g, rax, g.abi.sret_reg()) } else if instr.operands.len > 0 { ret_val_id := instr.operands[0] if g.value_is_float_type(ret_val_id) { @@ -1357,8 +1426,7 @@ fn (mut g Gen) emit_jmp(target_idx int) { } fn (mut g Gen) load_call_arg_to_reg(reg int, val_id int, arg_idx int, instr mir.Instruction) { - is_indirect := arg_idx >= 0 && arg_idx < instr.abi_arg_class.len - && instr.abi_arg_class[arg_idx] == .indirect + is_indirect := g.call_arg_is_indirect(val_id, arg_idx, instr) if is_indirect { g.load_address_of_val_to_reg(reg, val_id) return @@ -1376,14 +1444,105 @@ fn (mut g Gen) load_call_arg_to_reg(reg int, val_id int, arg_idx int, instr mir. g.load_val_to_reg(reg, val_id) } -fn (mut g Gen) load_call_register_args(instr mir.Instruction, abi_regs []int, stack_args []bool) int { +fn (mut g Gen) prepare_call_stack_args(instr mir.Instruction, stack_args []bool, stack_slots int, arg_position_base int) int { + if g.abi != .windows { + return 0 + } + cleanup := g.emit_windows_call_frame(stack_slots) + for arg_idx, is_stack in stack_args { + if is_stack { + g.store_windows_call_stack_arg(instr.operands[arg_idx + 1], arg_idx, arg_idx + + arg_position_base, instr) + } + } + return cleanup +} + +fn (mut g Gen) emit_windows_call_frame(stack_slots int) int { + if g.abi != .windows { + return 0 + } + cleanup := g.abi.call_frame_size(stack_slots) + if cleanup <= 127 { + asm_sub_rsp_imm8(mut g, u8(cleanup)) + } else { + asm_sub_rsp_imm32(mut g, u32(cleanup)) + } + return cleanup +} + +fn (mut g Gen) cleanup_windows_call_frame(cleanup int) { + if cleanup == 0 { + return + } + if cleanup <= 127 { + asm_add_rsp_imm8(mut g, u8(cleanup)) + } else { + asm_add_rsp_imm32(mut g, u32(cleanup)) + } +} + +fn (mut g Gen) store_windows_call_stack_arg(val_id int, arg_idx int, position int, instr mir.Instruction) { + is_indirect := g.call_arg_is_indirect(val_id, arg_idx, instr) + size := g.type_size(g.mod.values[val_id].typ) + g.ensure_windows_scalar_or_indirect_arg(val_id, is_indirect, size) + disp := g.abi.call_stack_arg_offset(position) + if g.value_is_float_type(val_id) { + g.ensure_float_abi_scalar(val_id, 'stack argument') + g.load_float_val_to_xmm(0, val_id, size) + asm_store_xmm_mem_base_disp_size(mut g, 0, rsp, disp, size) + return + } + g.load_call_arg_to_reg(0, val_id, arg_idx, instr) + store_size := if is_indirect { 8 } else { size } + asm_store_mem_base_disp_reg_size(mut g, rsp, disp, rax, store_size) +} + +fn (mut g Gen) ensure_windows_scalar_or_indirect_arg(val_id int, is_indirect bool, size int) { + if is_indirect { + return + } + if g.value_is_aggregate(val_id) && size !in [1, 2, 4, 8] { + g.unsupported_windows_abi_arg('aggregate arguments must be 1, 2, 4, or 8 bytes or passed indirectly', + val_id) + } +} + +fn (g Gen) unsupported_windows_abi_arg(reason string, val_id int) { + eprintln('x64: unsupported Windows ABI argument in value ${val_id}: ${reason}') + exit(1) +} + +fn (g Gen) windows_value_passed_indirect(val_id int, marked_indirect bool, size int) bool { + if marked_indirect { + return true + } + return g.abi == .windows && g.value_is_aggregate(val_id) && size !in [1, 2, 4, 8] +} + +fn (g Gen) call_arg_is_indirect(val_id int, arg_idx int, instr mir.Instruction) bool { + marked_indirect := arg_idx >= 0 && arg_idx < instr.abi_arg_class.len + && instr.abi_arg_class[arg_idx] == .indirect + return g.windows_value_passed_indirect(val_id, marked_indirect, + g.type_size(g.mod.values[val_id].typ)) +} + +fn (mut g Gen) load_call_register_args(instr mir.Instruction, abi_regs []int, stack_args []bool, arg_position_base int) int { + if g.abi.uses_positional_arg_regs() { + return g.load_windows_call_register_args(instr, stack_args, arg_position_base) + } + mut reg_arg_idx := 0 mut sse_arg_idx := 0 + float_arg_regs := g.abi.float_arg_regs() for i in 1 .. instr.operands.len { arg_idx := i - 1 arg_id := instr.operands[i] if g.value_is_float_type(arg_id) { - g.load_float_call_arg_to_xmm(sse_arg_idx, arg_id) + if sse_arg_idx >= float_arg_regs.len { + g.unsupported_float_abi('stack argument', arg_id) + } + g.load_float_call_arg_to_xmm(float_arg_regs[sse_arg_idx], arg_id) sse_arg_idx++ continue } @@ -1405,11 +1564,38 @@ fn (mut g Gen) load_call_register_args(instr mir.Instruction, abi_regs []int, st return sse_arg_idx } +fn (mut g Gen) load_windows_call_register_args(instr mir.Instruction, stack_args []bool, arg_position_base int) int { + mut sse_arg_count := 0 + for i in 1 .. instr.operands.len { + arg_idx := i - 1 + if stack_args[arg_idx] { + continue + } + arg_id := instr.operands[i] + position := arg_idx + arg_position_base + if g.value_is_float_type(arg_id) { + xmm := g.abi.float_arg_reg_for_position(position) + if xmm == x64_no_arg_reg { + g.unsupported_float_abi('stack argument', arg_id) + } + g.load_float_call_arg_to_xmm(xmm, arg_id) + sse_arg_count++ + continue + } + reg := g.abi.int_arg_reg_for_position(position) + if reg == x64_no_arg_reg { + continue + } + is_indirect := g.call_arg_is_indirect(arg_id, arg_idx, instr) + size := g.type_size(g.mod.values[arg_id].typ) + g.ensure_windows_scalar_or_indirect_arg(arg_id, is_indirect, size) + g.load_call_arg_to_reg(reg, arg_id, arg_idx, instr) + } + return sse_arg_count +} + fn (mut g Gen) load_float_call_arg_to_xmm(xmm int, val_id int) { g.ensure_float_abi_scalar(val_id, 'argument') - if xmm >= 8 { - g.unsupported_float_abi('stack argument', val_id) - } g.load_float_val_to_xmm(xmm, val_id, g.type_size(g.mod.values[val_id].typ)) } @@ -1423,6 +1609,9 @@ fn (mut g Gen) store_call_result(val_id int) { } fn (mut g Gen) emit_sse_arg_count(count int) { + if g.abi != .sysv { + return + } if count == 0 { asm_xor_eax_eax(mut g) } else { @@ -1444,8 +1633,7 @@ fn (g Gen) param_stack_slots(is_indirect bool, reg_chunks int, size int) int { } fn (g Gen) call_arg_reg_chunks(val_id int, arg_idx int, instr mir.Instruction) int { - is_indirect := arg_idx >= 0 && arg_idx < instr.abi_arg_class.len - && instr.abi_arg_class[arg_idx] == .indirect + is_indirect := g.call_arg_is_indirect(val_id, arg_idx, instr) if is_indirect || !g.value_is_aggregate(val_id) { return 1 } @@ -1457,8 +1645,7 @@ fn (g Gen) call_arg_reg_chunks(val_id int, arg_idx int, instr mir.Instruction) i } fn (g Gen) call_arg_stack_slots(val_id int, arg_idx int, instr mir.Instruction) int { - is_indirect := arg_idx >= 0 && arg_idx < instr.abi_arg_class.len - && instr.abi_arg_class[arg_idx] == .indirect + is_indirect := g.call_arg_is_indirect(val_id, arg_idx, instr) if is_indirect { return 1 } @@ -1469,16 +1656,37 @@ fn (g Gen) call_arg_stack_slots(val_id int, arg_idx int, instr mir.Instruction) return 1 } -fn (g Gen) call_stack_arg_mask(instr mir.Instruction, abi_reg_count int) []bool { +fn (g Gen) call_stack_arg_mask(instr mir.Instruction, abi_reg_count int, arg_position_base int) []bool { num_args := instr.operands.len - 1 mut stack_args := []bool{len: num_args} + if g.abi.uses_positional_arg_regs() { + for arg_idx := 0; arg_idx < num_args; arg_idx++ { + position := arg_idx + arg_position_base + if position >= g.abi.int_arg_regs().len { + stack_args[arg_idx] = true + continue + } + arg_id := instr.operands[arg_idx + 1] + if g.value_is_float_type(arg_id) { + g.ensure_float_abi_scalar(arg_id, 'argument') + continue + } + if g.call_arg_reg_chunks(arg_id, arg_idx, instr) > 1 { + g.unsupported_windows_abi_arg('aggregate register splitting is not supported', + arg_id) + } + } + return stack_args + } + mut reg_arg_idx := 0 mut sse_arg_idx := 0 + float_arg_regs := g.abi.float_arg_regs() for arg_idx := 0; arg_idx < num_args; arg_idx++ { arg_id := instr.operands[arg_idx + 1] if g.value_is_float_type(arg_id) { g.ensure_float_abi_scalar(arg_id, 'argument') - if sse_arg_idx >= 8 { + if sse_arg_idx >= float_arg_regs.len { g.unsupported_float_abi('stack argument', arg_id) } sse_arg_idx++ @@ -1505,8 +1713,7 @@ fn (g Gen) call_stack_slots(instr mir.Instruction, stack_args []bool) int { } fn (mut g Gen) push_call_stack_arg(val_id int, arg_idx int, instr mir.Instruction) { - is_indirect := arg_idx >= 0 && arg_idx < instr.abi_arg_class.len - && instr.abi_arg_class[arg_idx] == .indirect + is_indirect := g.call_arg_is_indirect(val_id, arg_idx, instr) size := g.type_size(g.mod.values[val_id].typ) slots := g.call_arg_stack_slots(val_id, arg_idx, instr) if !is_indirect && slots > 1 { diff --git a/vlib/v2/gen/x64/x64_abi_test.v b/vlib/v2/gen/x64/x64_abi_test.v new file mode 100644 index 000000000..d93985f58 --- /dev/null +++ b/vlib/v2/gen/x64/x64_abi_test.v @@ -0,0 +1,653 @@ +module x64 + +import v2.mir +import v2.ssa + +fn contains_bytes(haystack []u8, needle []u8) bool { + if needle.len == 0 { + return true + } + if needle.len > haystack.len { + return false + } + for i in 0 .. haystack.len - needle.len + 1 { + if haystack[i..i + needle.len] == needle { + return true + } + } + return false +} + +fn test_x64_abi_int_argument_registers() { + assert X64Abi.sysv.int_arg_regs() == [int(rdi), int(rsi), int(rdx), int(rcx), int(r8), int(r9)] + assert X64Abi.windows.int_arg_regs() == [int(rcx), int(rdx), int(r8), int(r9)] + assert X64Abi.sysv.int_arg_reg_at(0) == int(rdi) + assert X64Abi.sysv.int_arg_reg_at(5) == int(r9) + assert X64Abi.sysv.int_arg_reg_at(6) == x64_no_arg_reg +} + +fn test_x64_abi_float_argument_registers() { + assert X64Abi.sysv.float_arg_regs() == [0, 1, 2, 3, 4, 5, 6, 7] + assert X64Abi.windows.float_arg_regs() == [0, 1, 2, 3] + assert X64Abi.sysv.float_arg_reg_at(0) == 0 + assert X64Abi.sysv.float_arg_reg_at(7) == 7 + assert X64Abi.sysv.float_arg_reg_at(8) == x64_no_arg_reg +} + +fn test_x64_windows_argument_registers_are_position_based() { + assert X64Abi.windows.uses_positional_arg_regs() + assert X64Abi.windows.int_arg_reg_for_position(0) == int(rcx) + assert X64Abi.windows.float_arg_reg_for_position(1) == 1 + assert X64Abi.windows.int_arg_reg_for_position(2) == int(r8) + assert X64Abi.windows.float_arg_reg_for_position(3) == 3 + assert X64Abi.windows.int_arg_reg_for_position(4) == x64_no_arg_reg + assert X64Abi.windows.float_arg_reg_for_position(4) == x64_no_arg_reg +} + +fn test_x64_sysv_argument_helpers_keep_compact_register_order() { + assert !X64Abi.sysv.uses_positional_arg_regs() + assert X64Abi.sysv.int_arg_reg_at(0) == int(rdi) + assert X64Abi.sysv.int_arg_reg_at(1) == int(rsi) + assert X64Abi.sysv.float_arg_reg_at(0) == 0 + assert X64Abi.sysv.float_arg_reg_at(1) == 1 +} + +fn test_x64_abi_shadow_space_size() { + assert X64Abi.sysv.shadow_space_size() == 0 + assert X64Abi.windows.shadow_space_size() == 32 + assert X64Abi.windows.call_stack_arg_offset(4) == 32 + assert X64Abi.windows.call_stack_arg_offset(5) == 40 + assert X64Abi.windows.stack_arg_offset(4) == 48 + assert X64Abi.windows.stack_arg_offset(5) == 56 + assert X64Abi.windows.call_frame_size(0) == 32 + assert X64Abi.windows.call_frame_size(1) == 48 +} + +fn test_x64_abi_sret_registers_are_explicit() { + assert X64Abi.sysv.sret_reg() == rdi + assert X64Abi.sysv.sret_arg_regs() == [int(rsi), int(rdx), int(rcx), int(r8), int(r9)] + assert X64Abi.windows.sret_reg() == rcx + assert X64Abi.windows.sret_arg_regs() == [int(rdx), int(r8), int(r9)] +} + +fn test_x64_gen_defaults_to_sysv_abi() { + mut mod := mir.Module{} + gen := Gen.new(&mod) + assert gen.abi == .sysv + + coff_gen := Gen.new_with_format(&mod, .coff) + assert coff_gen.abi == .sysv + + windows_gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + assert windows_gen.abi == .windows +} + +fn test_x64_windows_stack_arg_mask_is_position_based() { + mut mod := new_x64_abi_test_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + instr := mir.Instruction{ + operands: [ssa.ValueID(0), 1, 2, 3, 4, 5] + } + + assert gen.call_stack_arg_mask(instr, gen.abi.int_arg_regs().len, 0) == [false, false, false, + false, true] + assert gen.call_stack_arg_mask(instr, gen.abi.sret_arg_regs().len, 1) == [false, false, false, + true, true] +} + +fn test_x64_sysv_stack_arg_mask_keeps_separate_int_and_float_counters() { + mut mod := new_x64_abi_test_module() + mut gen := Gen.new_with_format_and_abi(&mod, .elf, .sysv) + instr := mir.Instruction{ + operands: [ssa.ValueID(0), 1, 2, 3, 4, 5] + } + + assert gen.call_stack_arg_mask(instr, gen.abi.int_arg_regs().len, 0) == [false, false, false, + false, false] +} + +fn test_x64_windows_call_frame_reserves_shadow_space() { + mut mod := new_x64_abi_test_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + + cleanup := gen.emit_windows_call_frame(1) + assert cleanup == 48 + gen.cleanup_windows_call_frame(cleanup) + assert gen.coff.text_data == [u8(0x48), 0x83, 0xec, 0x30, 0x48, 0x83, 0xc4, 0x30] +} + +fn test_x64_windows_stack_arg_store_starts_after_shadow_space() { + mut mod := new_x64_abi_test_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + instr := mir.Instruction{ + operands: [ssa.ValueID(0), 1, 2, 3, 4, 5] + } + + gen.store_windows_call_stack_arg(5, 4, 4, instr) + assert gen.coff.text_data == [u8(0xb8), 0x05, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x20] +} + +fn test_x64_windows_does_not_emit_sysv_sse_arg_count() { + mut mod := new_x64_abi_test_module() + mut win_gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + win_gen.emit_sse_arg_count(2) + assert win_gen.coff.text_data.len == 0 + + mut sysv_gen := Gen.new_with_format_and_abi(&mod, .elf, .sysv) + sysv_gen.emit_sse_arg_count(2) + assert sysv_gen.elf.text_data == [u8(0xb8), 0x02, 0, 0, 0] +} + +fn test_x64_windows_codegen_mixed_call_uses_positional_registers() { + mut mod := new_x64_abi_call_module(false) + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x83, 0xec, 0x30]) + assert contains_bytes(code, [u8(0xb9), 0x01, 0, 0, 0]) // arg0 int -> RCX + assert contains_bytes(code, [u8(0xf2), 0x0f, 0x10, 0x4d]) // arg1 float -> XMM1 + assert contains_bytes(code, [u8(0x41), 0xb8, 0x03, 0, 0, 0]) // arg2 int -> R8 + assert contains_bytes(code, [u8(0xf2), 0x0f, 0x10, 0x5d]) // arg3 float -> XMM3 + assert contains_bytes(code, [u8(0xb8), 0x05, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x20]) + assert !contains_bytes(code, [u8(0xb8), 0x02, 0, 0, 0]) // no SysV AL SSE count +} + +fn test_x64_windows_codegen_call_sret_shifts_user_arg_registers() { + mut mod := new_x64_abi_call_module(true) + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x89, 0xc1]) // hidden sret pointer -> RCX + assert contains_bytes(code, [u8(0xba), 0x01, 0, 0, 0]) // first user int -> RDX + assert contains_bytes(code, [u8(0xf2), 0x0f, 0x10, 0x55]) // second user float -> XMM2 +} + +fn test_x64_windows_codegen_callee_stack_arg_loads_from_rbp_48() { + mut mod := new_x64_abi_callee_stack_arg_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x8b, 0x45, 0x30]) // mov rax, [rbp+48] +} + +fn test_x64_windows_codegen_large_aggregate_arg_uses_indirect_pointer() { + mut mod := new_x64_abi_indirect_aggregate_call_module(true) + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x83, 0xec, 0x20]) // shadow space + assert contains_bytes(code, [u8(0x48), 0x8d, 0x45]) // lea rax, [rbp+disp8] + assert contains_bytes(code, [u8(0x48), 0x89, 0xc1]) // indirect arg pointer -> RCX +} + +fn test_x64_windows_codegen_unmarked_large_aggregate_arg_uses_indirect_pointer() { + mut mod := new_x64_abi_indirect_aggregate_call_module(false) + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x83, 0xec, 0x20]) // shadow space + assert contains_bytes(code, [u8(0x48), 0x8d, 0x45]) // lea rax, [rbp+disp8] + assert contains_bytes(code, [u8(0x48), 0x89, 0xc1]) // indirect arg pointer -> RCX +} + +fn test_x64_windows_codegen_struct8_arg_remains_by_value() { + mut mod := new_x64_abi_struct8_call_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x49), 0x8b, 0x0a]) // mov rcx, [r10] + assert !contains_bytes(code, [u8(0x48), 0x89, 0xc1]) // no pointer move into RCX +} + +fn test_x64_windows_codegen_stack_large_aggregate_arg_uses_indirect_pointer() { + mut mod := new_x64_abi_stack_indirect_aggregate_call_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + code := gen.coff.text_data + + assert contains_bytes(code, [u8(0x48), 0x83, 0xec, 0x30]) // shadow space + one stack slot + assert contains_bytes(code, [u8(0x48), 0x8d, 0x45]) // lea rax, [rbp+disp8] + assert contains_bytes(code, [u8(0x48), 0x89, 0x44, 0x24, 0x20]) // pointer -> [rsp+32] +} + +fn new_x64_abi_test_module() mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + float_t := ts.get_float(64) + return mir.Module{ + type_store: unsafe { *ts } + values: [ + mir.Value{ + id: 0 + typ: int_t + kind: .func_ref + name: 'callee' + index: 0 + }, + mir.Value{ + id: 1 + typ: int_t + kind: .constant + name: '1' + index: 1 + }, + mir.Value{ + id: 2 + typ: float_t + kind: .constant + name: '2' + index: 2 + }, + mir.Value{ + id: 3 + typ: int_t + kind: .constant + name: '3' + index: 3 + }, + mir.Value{ + id: 4 + typ: float_t + kind: .constant + name: '4' + index: 4 + }, + mir.Value{ + id: 5 + typ: int_t + kind: .constant + name: '5' + index: 5 + }, + ] + } +} + +fn new_x64_abi_call_module(sret bool) mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + float_t := ts.get_float(64) + struct9_t := ts.register(ssa.Type{ + kind: .struct_t + fields: [int_t, ts.get_int(8)] + }) + mut m := mir.Module{ + type_store: unsafe { *ts } + values: []mir.Value{len: 8} + instrs: []mir.Instruction{len: 2} + blocks: []mir.BasicBlock{len: 1} + funcs: []mir.Function{len: 1} + } + m.values[0] = mir.Value{ + id: 0 + typ: int_t + kind: .func_ref + name: 'callee' + index: 0 + } + for i in 1 .. 6 { + m.values[i] = mir.Value{ + id: i + typ: if i in [2, 4] { float_t } else { int_t } + kind: .constant + name: i.str() + index: i + } + } + m.values[6] = mir.Value{ + id: 6 + typ: struct9_t + kind: .instruction + name: 'v6' + index: 0 + } + m.values[7] = mir.Value{ + id: 7 + typ: int_t + kind: .instruction + name: 'v7' + index: 1 + } + m.instrs[0] = mir.Instruction{ + op: if sret { .call_sret } else { .call } + operands: if sret { [ssa.ValueID(0), 1, 2] } else { [ssa.ValueID(0), 1, 2, 3, 4, 5] } + typ: if sret { struct9_t } else { int_t } + block: 0 + } + m.instrs[1] = mir.Instruction{ + op: .ret + operands: []ssa.ValueID{} + typ: 0 + block: 0 + } + m.blocks[0] = mir.BasicBlock{ + id: 0 + val_id: 0 + name: 'entry' + parent: 0 + instrs: [ssa.ValueID(6), 7] + } + m.funcs[0] = mir.Function{ + id: 0 + name: 'caller' + typ: 0 + blocks: [ssa.BlockID(0)] + } + return m +} + +fn new_x64_abi_struct8_call_module() mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + struct8_t := ts.register(ssa.Type{ + kind: .struct_t + fields: [int_t] + }) + mut m := mir.Module{ + type_store: unsafe { *ts } + values: []mir.Value{len: 5} + instrs: []mir.Instruction{len: 2} + blocks: []mir.BasicBlock{len: 1} + funcs: []mir.Function{len: 1} + } + m.values[0] = mir.Value{ + id: 0 + typ: int_t + kind: .func_ref + name: 'callee' + index: 0 + } + m.values[1] = mir.Value{ + id: 1 + typ: struct8_t + kind: .argument + name: 's' + index: 1 + } + m.values[2] = mir.Value{ + id: 2 + typ: int_t + kind: .instruction + name: 'v2' + index: 0 + } + m.values[3] = mir.Value{ + id: 3 + typ: int_t + kind: .instruction + name: 'v3' + index: 1 + } + m.values[4] = mir.Value{ + id: 4 + typ: int_t + kind: .basic_block + name: 'entry' + index: 0 + } + m.instrs[0] = mir.Instruction{ + op: .call + operands: [ssa.ValueID(0), 1] + typ: int_t + block: 0 + } + m.instrs[1] = mir.Instruction{ + op: .ret + operands: []ssa.ValueID{} + typ: 0 + block: 0 + } + m.blocks[0] = mir.BasicBlock{ + id: 0 + val_id: 4 + name: 'entry' + parent: 0 + instrs: [ssa.ValueID(2), 3] + } + m.funcs[0] = mir.Function{ + id: 0 + name: 'caller' + typ: 0 + blocks: [ssa.BlockID(0)] + params: [ssa.ValueID(1)] + } + return m +} + +fn new_x64_abi_stack_indirect_aggregate_call_module() mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + struct16_t := ts.register(ssa.Type{ + kind: .struct_t + fields: [int_t, int_t] + }) + mut m := mir.Module{ + type_store: unsafe { *ts } + values: []mir.Value{len: 10} + instrs: []mir.Instruction{len: 2} + blocks: []mir.BasicBlock{len: 1} + funcs: []mir.Function{len: 1} + } + m.values[0] = mir.Value{ + id: 0 + typ: int_t + kind: .func_ref + name: 'callee' + index: 0 + } + for i in 1 .. 5 { + m.values[i] = mir.Value{ + id: i + typ: int_t + kind: .constant + name: i.str() + index: i + } + } + m.values[5] = mir.Value{ + id: 5 + typ: struct16_t + kind: .argument + name: 's' + index: 5 + } + m.values[6] = mir.Value{ + id: 6 + typ: int_t + kind: .instruction + name: 'v6' + index: 0 + } + m.values[7] = mir.Value{ + id: 7 + typ: int_t + kind: .instruction + name: 'v7' + index: 1 + } + m.values[8] = mir.Value{ + id: 8 + typ: int_t + kind: .basic_block + name: 'entry' + index: 0 + } + m.values[9] = mir.Value{ + id: 9 + typ: int_t + kind: .constant + name: '0' + index: 9 + } + m.instrs[0] = mir.Instruction{ + op: .call + operands: [ssa.ValueID(0), 1, 2, 3, 4, 5] + typ: int_t + block: 0 + abi_arg_class: [.in_reg, .in_reg, .in_reg, .in_reg, .indirect] + } + m.instrs[1] = mir.Instruction{ + op: .ret + operands: []ssa.ValueID{} + typ: 0 + block: 0 + } + m.blocks[0] = mir.BasicBlock{ + id: 0 + val_id: 8 + name: 'entry' + parent: 0 + instrs: [ssa.ValueID(6), 7] + } + m.funcs[0] = mir.Function{ + id: 0 + name: 'caller' + typ: 0 + blocks: [ssa.BlockID(0)] + params: [ssa.ValueID(5)] + abi_param_class: [.indirect] + } + return m +} + +fn new_x64_abi_callee_stack_arg_module() mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + mut m := mir.Module{ + type_store: unsafe { *ts } + values: []mir.Value{len: 7} + instrs: []mir.Instruction{len: 1} + blocks: []mir.BasicBlock{len: 1} + funcs: []mir.Function{len: 1} + } + for i in 0 .. 5 { + m.values[i] = mir.Value{ + id: i + typ: int_t + kind: .argument + name: 'a${i}' + index: i + } + } + m.values[5] = mir.Value{ + id: 5 + typ: int_t + kind: .instruction + name: 'v5' + index: 0 + } + m.values[6] = mir.Value{ + id: 6 + typ: int_t + kind: .basic_block + name: 'entry' + index: 0 + } + m.instrs[0] = mir.Instruction{ + op: .ret + operands: []ssa.ValueID{} + typ: 0 + block: 0 + } + m.blocks[0] = mir.BasicBlock{ + id: 0 + val_id: 6 + name: 'entry' + parent: 0 + instrs: [ssa.ValueID(5)] + } + m.funcs[0] = mir.Function{ + id: 0 + name: 'callee' + typ: 0 + blocks: [ssa.BlockID(0)] + params: [ssa.ValueID(0), 1, 2, 3, 4] + } + return m +} + +fn new_x64_abi_indirect_aggregate_call_module(mark_indirect bool) mir.Module { + mut ts := ssa.TypeStore.new() + int_t := ts.get_int(64) + struct16_t := ts.register(ssa.Type{ + kind: .struct_t + fields: [int_t, int_t] + }) + mut m := mir.Module{ + type_store: unsafe { *ts } + values: []mir.Value{len: 5} + instrs: []mir.Instruction{len: 2} + blocks: []mir.BasicBlock{len: 1} + funcs: []mir.Function{len: 1} + } + m.values[0] = mir.Value{ + id: 0 + typ: int_t + kind: .func_ref + name: 'callee' + index: 0 + } + m.values[1] = mir.Value{ + id: 1 + typ: struct16_t + kind: .argument + name: 's' + index: 1 + } + m.values[2] = mir.Value{ + id: 2 + typ: int_t + kind: .instruction + name: 'v2' + index: 0 + } + m.values[3] = mir.Value{ + id: 3 + typ: int_t + kind: .instruction + name: 'v3' + index: 1 + } + m.values[4] = mir.Value{ + id: 4 + typ: int_t + kind: .basic_block + name: 'entry' + index: 0 + } + m.instrs[0] = mir.Instruction{ + op: .call + operands: [ssa.ValueID(0), 1] + typ: int_t + block: 0 + abi_arg_class: if mark_indirect { [.indirect] } else { []mir.AbiArgClass{} } + } + m.instrs[1] = mir.Instruction{ + op: .ret + operands: []ssa.ValueID{} + typ: 0 + block: 0 + } + m.blocks[0] = mir.BasicBlock{ + id: 0 + val_id: 4 + name: 'entry' + parent: 0 + instrs: [ssa.ValueID(2), 3] + } + m.funcs[0] = mir.Function{ + id: 0 + name: 'caller' + typ: 0 + blocks: [ssa.BlockID(0)] + params: [ssa.ValueID(1)] + abi_param_class: if mark_indirect { [.indirect] } else { []mir.AbiArgClass{} } + } + return m +} diff --git a/vlib/v2/gen/x64/x64_object_format_test.v b/vlib/v2/gen/x64/x64_object_format_test.v index 63db74a1e..042295672 100644 --- a/vlib/v2/gen/x64/x64_object_format_test.v +++ b/vlib/v2/gen/x64/x64_object_format_test.v @@ -11,6 +11,10 @@ fn read_u32_le(data []u8, off int) u32 { off + 3]) << 24) } +fn read_u16_le(data []u8, off int) u16 { + return u16(data[off]) | (u16(data[off + 1]) << 8) +} + fn read_string(data []u8, off int) string { mut end := off for end < data.len && data[end] != 0 { @@ -103,6 +107,9 @@ fn test_macho_object_format_prefixes_external_symbols() { assert ObjectFormat.elf.symbol_name('main') == 'main' assert ObjectFormat.elf.symbol_name('calloc') == 'calloc' assert ObjectFormat.elf.symbol_name('__stdoutp') == '__stdoutp' + assert ObjectFormat.coff.symbol_name('main') == 'main' + assert ObjectFormat.coff.symbol_name('calloc') == 'calloc' + assert ObjectFormat.coff.symbol_name('__stdoutp') == '__stdoutp' } fn test_elf_writer_reuses_undefined_symbol_when_it_is_defined_later() { @@ -198,3 +205,146 @@ fn test_macho_writer_serializes_text_relocations_and_symbols() { assert data[sym_off + 16 + 4] == 0x0e assert data[sym_off + 16 + 5] == 2 } + +fn test_coff_writer_emits_x64_relocatable_object() { + path := temp_object_path('coff') + defer { + os.rm(path) or {} + } + + mut obj := CoffObject.new() + obj.text_data << u8(0xc3) + obj.rodata << 'hello'.bytes() + obj.rodata << 0 + obj.data_data << [u8(1), 2, 3, 4] + obj.add_symbol('main', 0, true, 1) + obj.write(path) + + data := os.read_bytes(path) or { panic(err) } + assert data.len > 20 + (3 * 40) + assert read_u16_le(data, 0) == coff_image_file_machine_amd64 + assert read_u16_le(data, 2) == 3 + assert read_u16_le(data, 16) == 0 + assert data[20..28].bytestr().trim_right('\0') == '.text' + assert data[60..68].bytestr().trim_right('\0') == '.rdata' + assert data[100..108].bytestr().trim_right('\0') == '.data' + assert read_u32_le(data, 8) == 156 + assert read_u32_le(data, 20 + 16) == 1 + assert read_u32_le(data, 20 + 20) == 140 + assert read_u32_le(data, 20 + 20) % 4 == 0 + assert read_u32_le(data, 60 + 16) == 6 + assert read_u32_le(data, 60 + 20) == 144 + assert read_u32_le(data, 60 + 20) % 4 == 0 + assert read_u32_le(data, 100 + 16) == 4 + assert read_u32_le(data, 100 + 20) == 152 + assert read_u32_le(data, 100 + 20) % 4 == 0 + assert read_u32_le(data, 8) % 4 == 0 + assert read_u32_le(data, 20 + 36) & coff_image_scn_mem_execute != 0 + assert read_u32_le(data, 60 + 36) & coff_image_scn_mem_read != 0 + assert read_u32_le(data, 100 + 36) & coff_image_scn_mem_write != 0 +} + +fn test_coff_writer_serializes_symbols_and_string_table() { + path := temp_object_path('coff_symbols') + defer { + os.rm(path) or {} + } + + mut obj := CoffObject.new() + undef_idx := obj.add_undefined('external_long_symbol_name') + pure_undef_idx := obj.add_undefined('calloc') + main_idx := obj.add_symbol('main', 0, true, 1) + defined_idx := obj.add_symbol('global_long_symbol_name', 16, false, 3) + local_idx := obj.add_symbol('L_local_data', 4, false, 2) + late_def_idx := obj.add_symbol('external_long_symbol_name', 8, false, 2) + obj.write(path) + + assert late_def_idx == undef_idx + + data := os.read_bytes(path) or { panic(err) } + sym_off := int(read_u32_le(data, 8)) + nsyms := int(read_u32_le(data, 12)) + assert nsyms == 5 + + string_table_off := sym_off + (nsyms * 18) + string_table_size := int(read_u32_le(data, string_table_off)) + assert string_table_size > 4 + + main_off := sym_off + (main_idx * 18) + assert data[main_off..main_off + 8].bytestr().trim_right('\0') == 'main' + assert read_u32_le(data, main_off + 8) == 0 + assert read_u16_le(data, main_off + 12) == 1 + assert read_u16_le(data, main_off + 14) == coff_image_sym_dtype_function + assert data[main_off + 16] == coff_image_sym_class_external + + defined_off := sym_off + (defined_idx * 18) + assert read_u32_le(data, defined_off) == 0 + defined_name_off := int(read_u32_le(data, defined_off + 4)) + assert read_string(data, string_table_off + defined_name_off) == 'global_long_symbol_name' + assert read_u32_le(data, defined_off + 8) == 16 + assert read_u16_le(data, defined_off + 12) == 3 + + late_def_off := sym_off + (late_def_idx * 18) + late_def_name_off := int(read_u32_le(data, late_def_off + 4)) + assert read_string(data, string_table_off + late_def_name_off) == 'external_long_symbol_name' + assert read_u32_le(data, late_def_off + 8) == 8 + assert read_u16_le(data, late_def_off + 12) == 2 + + pure_undef_off := sym_off + (pure_undef_idx * 18) + assert data[pure_undef_off..pure_undef_off + 8].bytestr().trim_right('\0') == 'calloc' + assert read_u32_le(data, pure_undef_off + 8) == 0 + assert read_u16_le(data, pure_undef_off + 12) == 0 + assert data[pure_undef_off + 16] == coff_image_sym_class_external + + local_off := sym_off + (local_idx * 18) + assert read_u32_le(data, local_off) == 0 + local_name_off := int(read_u32_le(data, local_off + 4)) + assert read_string(data, string_table_off + local_name_off) == 'L_local_data' + assert read_u16_le(data, local_off + 12) == 2 + assert data[local_off + 16] == coff_image_sym_class_static +} + +fn test_coff_writer_serializes_text_relocations() { + path := temp_object_path('coff_relocs') + defer { + os.rm(path) or {} + } + + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, 0, 0, 0] + call_sym := obj.add_undefined('calloc') + data_sym := obj.add_symbol('L_str_0', 0, false, 2) + obj.add_text_reloc(1, call_sym, coff_image_rel_amd64_rel32) + obj.add_text_reloc(8, data_sym, coff_image_rel_amd64_rel32) + obj.write(path) + + data := os.read_bytes(path) or { panic(err) } + text_section_off := 20 + reloc_off := int(read_u32_le(data, text_section_off + 24)) + nreloc := int(read_u16_le(data, text_section_off + 32)) + assert nreloc == 2 + assert reloc_off == 152 + assert reloc_off % 4 == 0 + assert read_u32_le(data, 8) == 172 + assert read_u32_le(data, 8) % 4 == 0 + + assert read_u32_le(data, reloc_off) == 1 + assert read_u32_le(data, reloc_off + 4) == u32(call_sym) + assert read_u16_le(data, reloc_off + 8) == coff_image_rel_amd64_rel32 + assert read_u32_le(data, reloc_off + 10) == 8 + assert read_u32_le(data, reloc_off + 14) == u32(data_sym) + assert read_u16_le(data, reloc_off + 18) == coff_image_rel_amd64_rel32 +} + +fn test_coff_writer_rejects_relocation_count_overflow() { + section := CoffSection{ + name: '.text' + relocs: []CoffRelocation{len: 0x10000} + } + if _ := coff_relocation_count_for_header(section) { + assert false + } else { + assert err.msg().contains('COFF section .text has 65536 relocations') + assert err.msg().contains('extended relocations are not supported') + } +} diff --git a/vlib/v2/gen/x64/x64_pe_linker_test.v b/vlib/v2/gen/x64/x64_pe_linker_test.v new file mode 100644 index 000000000..fe6654ce6 --- /dev/null +++ b/vlib/v2/gen/x64/x64_pe_linker_test.v @@ -0,0 +1,363 @@ +module x64 + +import os +import v2.mir + +fn pe_test_u16(data []u8, off int) u16 { + return u16(data[off]) | (u16(data[off + 1]) << 8) +} + +fn pe_test_u32(data []u8, off int) u32 { + return u32(data[off]) | (u32(data[off + 1]) << 8) | (u32(data[off + 2]) << 16) | (u32(data[ + off + 3]) << 24) +} + +fn pe_test_u64(data []u8, off int) u64 { + return u64(pe_test_u32(data, off)) | (u64(pe_test_u32(data, off + 4)) << 32) +} + +fn pe_test_i32(data []u8, off int) i32 { + return i32(pe_test_u32(data, off)) +} + +fn pe_test_string(data []u8, off int) string { + mut end := off + for end < data.len && data[end] != 0 { + end++ + } + return data[off..end].bytestr() +} + +fn pe_test_section_header(image []u8, name string) int { + pe_off := int(pe_test_u32(image, 0x3c)) + nsections := int(pe_test_u16(image, pe_off + 6)) + section_off := pe_off + 4 + 20 + int(pe_test_u16(image, pe_off + 20)) + for i in 0 .. nsections { + off := section_off + i * 40 + if image[off..off + 8].bytestr().trim_right('\0') == name { + return off + } + } + return -1 +} + +fn pe_test_rva_to_file_off(image []u8, rva u32) int { + pe_off := int(pe_test_u32(image, 0x3c)) + nsections := int(pe_test_u16(image, pe_off + 6)) + section_off := pe_off + 4 + 20 + int(pe_test_u16(image, pe_off + 20)) + for i in 0 .. nsections { + off := section_off + i * 40 + va := pe_test_u32(image, off + 12) + raw_size := pe_test_u32(image, off + 16) + raw_ptr := pe_test_u32(image, off + 20) + virtual_size := pe_test_u32(image, off + 8) + size := if virtual_size > raw_size { virtual_size } else { raw_size } + if rva >= va && rva < va + size { + return int(raw_ptr + (rva - va)) + } + } + return -1 +} + +fn sample_pe_coff_object() &CoffObject { + mut obj := CoffObject.new() + obj.text_data << [u8(0xc3), 0xc3] + obj.rodata << 'Hello World!'.bytes() + obj.rodata << 0 + obj.data_data << [u8(1), 2, 3, 4] + obj.add_symbol('_vinit', 0, true, 1) + obj.add_symbol('main', 1, true, 1) + return obj +} + +fn test_pe_linker_emits_pe32_plus_headers_and_sections() { + obj := sample_pe_coff_object() + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + + assert image[0] == `M` + assert image[1] == `Z` + pe_off := int(pe_test_u32(image, 0x3c)) + assert pe_off == pe_dos_stub_size + assert pe_test_u32(image, pe_off) == pe_signature + assert pe_test_u16(image, pe_off + 4) == coff_image_file_machine_amd64 + assert pe_test_u16(image, pe_off + 6) == 4 + assert pe_test_u16(image, pe_off + 20) == pe_size_of_optional_header64 + assert pe_test_u16(image, pe_off + 22) & pe_image_file_relocs_stripped != 0 + assert pe_test_u16(image, pe_off + 22) & pe_image_file_executable_image != 0 + + opt_off := pe_off + 4 + 20 + assert pe_test_u16(image, opt_off) == pe_optional_header64_magic + assert pe_test_u32(image, opt_off + 16) == pe_section_alignment + assert pe_test_u32(image, opt_off + 20) == pe_section_alignment + assert pe_test_u64(image, opt_off + 24) == pe_image_base + assert pe_test_u32(image, opt_off + 32) == pe_section_alignment + assert pe_test_u32(image, opt_off + 36) == pe_file_alignment + assert pe_test_u32(image, opt_off + 60) == pe_headers_size(4) + assert pe_test_u16(image, opt_off + 68) == pe_image_subsystem_windows_cui + assert pe_test_u16(image, opt_off + 70) & pe_dll_characteristics_nx_compat != 0 + assert pe_test_u32(image, opt_off + 108) == pe_number_of_rva_and_sizes + + text_off := pe_test_section_header(image, '.text') + rdata_off := pe_test_section_header(image, '.rdata') + data_off := pe_test_section_header(image, '.data') + idata_off := pe_test_section_header(image, '.idata') + assert text_off > 0 + assert rdata_off > text_off + assert data_off > rdata_off + assert idata_off > data_off + assert pe_test_u32(image, text_off + 12) == pe_section_alignment + assert pe_test_u32(image, text_off + 20) == pe_headers_size(4) + assert pe_test_u32(image, text_off + 20) % pe_file_alignment == 0 + assert pe_test_u32(image, text_off + 16) % pe_file_alignment == 0 + assert pe_test_u32(image, text_off + 36) & pe_image_scn_cnt_code != 0 + assert pe_test_u32(image, text_off + 36) & pe_image_scn_mem_execute != 0 + assert pe_test_u32(image, rdata_off + 12) % pe_section_alignment == 0 + assert pe_test_u32(image, rdata_off + 36) & pe_image_scn_mem_read != 0 + assert pe_test_u32(image, data_off + 36) & pe_image_scn_mem_write != 0 + assert pe_test_u32(image, idata_off + 12) % pe_section_alignment == 0 + assert pe_test_u32(image, idata_off + 36) & pe_image_scn_mem_write != 0 + + size_of_image := pe_test_u32(image, opt_off + 56) + idata_end := pe_test_u32(image, idata_off + 12) + pe_section_alignment + assert size_of_image >= idata_end +} + +fn test_pe_linker_emits_kernel32_import_table() { + obj := sample_pe_coff_object() + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + import_rva := pe_test_u32(image, opt_off + 112 + pe_import_directory_index * 8) + import_size := pe_test_u32(image, opt_off + 116 + pe_import_directory_index * 8) + iat_rva := pe_test_u32(image, opt_off + 112 + pe_iat_directory_index * 8) + iat_size := pe_test_u32(image, opt_off + 116 + pe_iat_directory_index * 8) + assert import_rva != 0 + assert import_size == 40 + assert iat_rva != 0 + assert iat_size == u32((pe_kernel32_imports.len + 1) * 8) + + import_off := pe_test_rva_to_file_off(image, import_rva) + assert import_off > 0 + ilt_rva := pe_test_u32(image, import_off) + dll_name_rva := pe_test_u32(image, import_off + 12) + first_thunk_rva := pe_test_u32(image, import_off + 16) + assert ilt_rva != 0 + assert first_thunk_rva == iat_rva + assert pe_test_string(image, pe_test_rva_to_file_off(image, dll_name_rva)) == 'kernel32.dll' + for i in 0 .. 20 { + assert image[import_off + 20 + i] == 0 + } + + mut names := []string{} + ilt_off := pe_test_rva_to_file_off(image, ilt_rva) + iat_off := pe_test_rva_to_file_off(image, iat_rva) + for i in 0 .. pe_kernel32_imports.len { + hint_name_rva := u32(pe_test_u64(image, ilt_off + i * 8)) + hint_name_off := pe_test_rva_to_file_off(image, hint_name_rva) + assert pe_test_u16(image, hint_name_off) == 0 + assert hint_name_off % 2 == 0 + names << pe_test_string(image, hint_name_off + 2) + assert pe_test_u64(image, iat_off + i * 8) == pe_test_u64(image, ilt_off + i * 8) + } + assert pe_test_u64(image, ilt_off + pe_kernel32_imports.len * 8) == 0 + assert pe_test_u64(image, iat_off + pe_kernel32_imports.len * 8) == 0 + assert names == pe_kernel32_imports +} + +fn test_pe_linker_zeroes_unemitted_data_directories() { + obj := sample_pe_coff_object() + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + for i in 0 .. pe_number_of_rva_and_sizes { + if i in [pe_import_directory_index, pe_iat_directory_index] { + continue + } + assert pe_test_u32(image, opt_off + 112 + i * 8) == 0 + assert pe_test_u32(image, opt_off + 116 + i * 8) == 0 + } + assert pe_test_u32(image, opt_off + 112 + pe_exception_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 116 + pe_exception_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 112 + pe_base_reloc_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 116 + pe_base_reloc_directory_index * 8) == 0 +} + +fn test_pe_linker_marks_fixed_base_image_without_unwind_or_reloc_directories() { + obj := sample_pe_coff_object() + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + + pe_off := int(pe_test_u32(image, 0x3c)) + characteristics := pe_test_u16(image, pe_off + 22) + assert characteristics & pe_image_file_relocs_stripped != 0 + + opt_off := pe_off + 4 + 20 + assert pe_test_u32(image, opt_off + 112 + pe_base_reloc_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 116 + pe_base_reloc_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 112 + pe_exception_directory_index * 8) == 0 + assert pe_test_u32(image, opt_off + 116 + pe_exception_directory_index * 8) == 0 +} + +fn test_pe_linker_empty_rdata_and_data_have_no_raw_storage_but_stable_rvas() { + mut obj := CoffObject.new() + obj.text_data << u8(0xc3) + obj.add_symbol('main', 0, true, 1) + + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + text_off := pe_test_section_header(image, '.text') + rdata_off := pe_test_section_header(image, '.rdata') + data_off := pe_test_section_header(image, '.data') + idata_off := pe_test_section_header(image, '.idata') + + assert pe_test_u32(image, rdata_off + 8) == 0 + assert pe_test_u32(image, rdata_off + 16) == 0 + assert pe_test_u32(image, rdata_off + 20) == 0 + assert pe_test_u32(image, data_off + 8) == 0 + assert pe_test_u32(image, data_off + 16) == 0 + assert pe_test_u32(image, data_off + 20) == 0 + + assert pe_test_u32(image, text_off + 12) == pe_section_alignment + assert pe_test_u32(image, rdata_off + 12) == pe_section_alignment * 2 + assert pe_test_u32(image, data_off + 12) == pe_section_alignment * 3 + assert pe_test_u32(image, idata_off + 12) == pe_section_alignment * 4 + assert pe_test_u32(image, idata_off + 20) == pe_headers_size(4) + pe_file_alignment + assert pe_test_u32(image, opt_off + 56) == pe_section_alignment * 5 +} + +fn test_pe_linker_entry_stub_targets_main_and_exitprocess_thunk() { + obj := sample_pe_coff_object() + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + + text_off := pe_test_section_header(image, '.text') + text_rva := pe_test_u32(image, text_off + 12) + text_raw := int(pe_test_u32(image, text_off + 20)) + entry_stub_len := linker.entry_stub_size() + + assert image[text_raw..text_raw + 4] == [u8(0x48), 0x83, 0xec, 0x28] + assert image[text_raw + entry_stub_len - 1] == 0xcc + + exit_thunk_off := text_raw + entry_stub_len + obj.text_data.len + assert image[exit_thunk_off] == 0xff + assert image[exit_thunk_off + 1] == 0x25 + + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + iat_rva := pe_test_u32(image, opt_off + 112 + pe_iat_directory_index * 8) + exit_thunk_rva := text_rva + u32(entry_stub_len + obj.text_data.len) + exit_disp := pe_test_i32(image, exit_thunk_off + 2) + assert u32(i32(exit_thunk_rva + 6) + exit_disp) == iat_rva +} + +fn test_pe_linker_applies_internal_rel32_relocations() { + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + obj.add_symbol('main', 0, true, 1) + helper_sym := obj.add_symbol('helper', 5, true, 1) + obj.add_text_reloc(1, helper_sym, coff_image_rel_amd64_rel32) + + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + text_off := pe_test_section_header(image, '.text') + text_raw := int(pe_test_u32(image, text_off + 20)) + field_off := text_raw + linker.entry_stub_size() + 1 + assert pe_test_i32(image, field_off) == 0 +} + +fn test_pe_linker_applies_import_rel32_relocations_to_thunks() { + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + obj.add_symbol('main', 0, true, 1) + exit_sym := obj.add_undefined('ExitProcess') + obj.add_text_reloc(1, exit_sym, coff_image_rel_amd64_rel32) + + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + text_off := pe_test_section_header(image, '.text') + text_rva := pe_test_u32(image, text_off + 12) + text_raw := int(pe_test_u32(image, text_off + 20)) + field_off := text_raw + linker.entry_stub_size() + 1 + field_rva := text_rva + u32(linker.entry_stub_size() + 1) + disp := pe_test_i32(image, field_off) + target := u32(i32(field_rva + 4) + disp) + assert target == text_rva + u32(linker.entry_stub_size() + obj.text_data.len) +} + +fn test_pe_linker_rejects_unsupported_external_symbols() { + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + obj.add_symbol('main', 5, true, 1) + calloc_sym := obj.add_undefined('calloc') + obj.add_text_reloc(1, calloc_sym, coff_image_rel_amd64_rel32) + + mut linker := PeLinker.new(obj) + if _ := linker.image() { + assert false + } else { + assert err.msg().contains('cannot resolve external symbol `calloc` yet') + } +} + +fn test_x64_gen_link_executable_writes_pe_image() { + path := os.join_path(os.temp_dir(), 'v_x64_pe_link_api_test.exe') + os.rm(path) or {} + defer { + os.rm(path) or {} + } + + mut mod := mir.Module{} + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.coff.text_data << u8(0xc3) + gen.coff.add_symbol('main', 0, true, 1) + + gen.link_executable(path) or { panic(err) } + image := os.read_bytes(path) or { panic(err) } + + assert image[0] == `M` + assert image[1] == `Z` + pe_off := int(pe_test_u32(image, 0x3c)) + assert pe_test_u32(image, pe_off) == pe_signature + assert pe_test_u16(image, pe_off + 4) == coff_image_file_machine_amd64 +} + +fn test_x64_gen_link_executable_requires_windows_abi() { + path := os.join_path(os.temp_dir(), 'v_x64_pe_link_api_sysv_test.exe') + defer { + os.rm(path) or {} + } + + mut mod := mir.Module{} + mut gen := Gen.new_with_format(&mod, .coff) + gen.coff.text_data << u8(0xc3) + gen.coff.add_symbol('main', 0, true, 1) + + if _ := gen.link_executable(path) { + assert false + } else { + assert err.msg().contains('requires Windows ABI') + } +} + +fn test_x64_gen_link_executable_rejects_non_coff_format() { + path := os.join_path(os.temp_dir(), 'v_x64_pe_link_api_elf_test.exe') + defer { + os.rm(path) or {} + } + + mut mod := mir.Module{} + mut gen := Gen.new_with_format_and_abi(&mod, .elf, .sysv) + + if _ := gen.link_executable(path) { + assert false + } else { + assert err.msg().contains('only implemented for COFF') + } +} diff --git a/vlib/v2/markused/markused.v b/vlib/v2/markused/markused.v index efcd8d58e..087e81762 100644 --- a/vlib/v2/markused/markused.v +++ b/vlib/v2/markused/markused.v @@ -81,6 +81,7 @@ struct FnInfo { struct Walker { files []ast.File env &types.Environment = unsafe { nil } + opts MarkUsedOptions mut: fns []FnInfo queue []int @@ -104,10 +105,20 @@ mut: cur_fn_decl ast.FnDecl } +pub struct MarkUsedOptions { +pub: + minimal_runtime_roots bool +} + // mark_used walks reachable function/method bodies and returns declaration keys // for the functions that are used at least once. pub fn mark_used(files []ast.File, env &types.Environment) map[string]bool { - mut w := new_walker(files, env) + mut w := new_walker(files, env, MarkUsedOptions{}) + return w.walk() +} + +pub fn mark_used_with_options(files []ast.File, env &types.Environment, opts MarkUsedOptions) map[string]bool { + mut w := new_walker(files, env, opts) return w.walk() } @@ -121,10 +132,11 @@ pub fn decl_key(module_name string, decl ast.FnDecl, env &types.Environment) str return '${mod_name}|f|${decl.name}' } -fn new_walker(files []ast.File, env &types.Environment) Walker { +fn new_walker(files []ast.File, env &types.Environment, opts MarkUsedOptions) Walker { return Walker{ files: files env: unsafe { env } + opts: opts used_keys: map[string]bool{} queued_fn_indices: map[int]bool{} module_names: map[string]bool{} @@ -234,16 +246,18 @@ fn (mut w Walker) seed_roots() bool { } } if has_root { - w.seed_generic_specialization_roots() - w.seed_codegen_required_roots() - // Also seed module init() functions (called from synthesized main) - for i, info in w.fns { - if is_module_init(info) { - w.mark_fn(i) + if !w.opts.minimal_runtime_roots { + w.seed_generic_specialization_roots() + w.seed_codegen_required_roots() + // Also seed module init() functions (called from synthesized main) + for i, info in w.fns { + if is_module_init(info) { + w.mark_fn(i) + } } + w.seed_top_level_initializer_roots() + w.seed_drop_method_roots() } - w.seed_top_level_initializer_roots() - w.seed_drop_method_roots() return true } for i, info in w.fns { @@ -253,17 +267,19 @@ fn (mut w Walker) seed_roots() bool { } } if has_root { - w.seed_generic_specialization_roots() - w.seed_codegen_required_roots() - w.seed_top_level_initializer_roots() - w.seed_drop_method_roots() + if !w.opts.minimal_runtime_roots { + w.seed_generic_specialization_roots() + w.seed_codegen_required_roots() + w.seed_top_level_initializer_roots() + w.seed_drop_method_roots() + } } return has_root } fn (mut w Walker) seed_codegen_required_roots() { for i, info in w.fns { - if is_codegen_required_root(info) { + if w.is_codegen_required_root(info) { w.mark_fn(i) } } @@ -290,12 +306,12 @@ fn (mut w Walker) seed_drop_method_roots() { } } -fn is_codegen_required_root(info FnInfo) bool { +fn (w &Walker) is_codegen_required_root(info FnInfo) bool { decl := info.decl - if should_always_emit_for_markused(info.file) { + if !w.opts.minimal_runtime_roots && should_always_emit_for_markused(info.file) { return true } - if info.mod == 'builtin' && decl.name == 'print_backtrace' { + if !w.opts.minimal_runtime_roots && info.mod == 'builtin' && decl.name == 'print_backtrace' { return true } if info.mod == 'json2' && decl.name == 'enum_uses_json_as_number' { diff --git a/vlib/v2/pref/comptime.v b/vlib/v2/pref/comptime.v index 5c9cf3d4e..2c83c1fc4 100644 --- a/vlib/v2/pref/comptime.v +++ b/vlib/v2/pref/comptime.v @@ -12,52 +12,34 @@ module pref pub fn comptime_flag_value(pref &Preferences, name string) bool { match name { 'macos', 'darwin' { - $if macos { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'macos' } 'linux' { - $if linux { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'linux' } 'windows' { - $if windows { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'windows' } 'bsd' { - $if macos || freebsd || openbsd || netbsd || dragonfly { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) in [ + 'macos', + 'freebsd', + 'openbsd', + 'netbsd', + 'dragonfly', + ] } 'freebsd' { - $if freebsd { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'freebsd' } 'openbsd' { - $if openbsd { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'openbsd' } 'netbsd' { - $if netbsd { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'netbsd' } 'dragonfly' { - $if dragonfly { - return true - } - return false + return normalize_current_os_name(pref.target_os_or_host()) == 'dragonfly' } 'x64', 'amd64' { $if amd64 { @@ -92,6 +74,11 @@ pub fn comptime_flag_value(pref &Preferences, name string) bool { 'native' { return pref != unsafe { nil } && (pref.backend == .arm64 || pref.backend == .x64) } + 'v2_native_windows_pe_minimal' { + return pref != unsafe { nil } && pref.backend == .x64 + && pref.get_effective_arch() == .x64 + && normalize_current_os_name(pref.target_os_or_host()) == 'windows' + } // Native backend cannot resolve C.stdout/C.stderr data symbols through GOT, // so use C.write() instead of fwrite() for I/O operations. 'builtin_write_buf_to_fd_should_use_c_write' { diff --git a/vlib/v2/pref/comptime_test.v b/vlib/v2/pref/comptime_test.v new file mode 100644 index 000000000..4903d7e2d --- /dev/null +++ b/vlib/v2/pref/comptime_test.v @@ -0,0 +1,45 @@ +module pref + +fn test_comptime_flag_value_uses_target_os_preference() { + mut prefs := new_preferences() + prefs.target_os = 'windows' + assert comptime_flag_value(&prefs, 'windows') + assert !comptime_flag_value(&prefs, 'linux') + assert !comptime_flag_value(&prefs, 'macos') + + prefs.target_os = 'darwin' + assert comptime_flag_value(&prefs, 'macos') + assert comptime_flag_value(&prefs, 'darwin') + assert comptime_flag_value(&prefs, 'bsd') + assert !comptime_flag_value(&prefs, 'windows') +} + +fn test_comptime_flag_value_allows_nil_preferences() { + prefs := unsafe { &Preferences(nil) } + assert comptime_flag_value(prefs, 'linux') == (prefs.target_os_or_host() == 'linux') + assert !comptime_flag_value(prefs, 'definitely_missing_flag') +} + +fn test_effective_arch_uses_target_os_preference() { + mut prefs := new_preferences() + prefs.arch = .auto + prefs.target_os = 'macos' + assert prefs.get_effective_arch() == .arm64 + + prefs.target_os = 'windows' + assert prefs.get_effective_arch() == .x64 +} + +fn test_v2_native_windows_pe_minimal_flag() { + mut prefs := new_preferences() + prefs.backend = .x64 + prefs.arch = .x64 + prefs.target_os = 'windows' + assert comptime_flag_value(&prefs, 'v2_native_windows_pe_minimal') + + prefs.target_os = 'linux' + assert !comptime_flag_value(&prefs, 'v2_native_windows_pe_minimal') + prefs.target_os = 'windows' + prefs.backend = .arm64 + assert !comptime_flag_value(&prefs, 'v2_native_windows_pe_minimal') +} diff --git a/vlib/v2/pref/pref.v b/vlib/v2/pref/pref.v index b53018586..0ccbee83f 100644 --- a/vlib/v2/pref/pref.v +++ b/vlib/v2/pref/pref.v @@ -54,7 +54,8 @@ pub mut: prealloc bool // -prealloc: use arena allocation (bump-pointer, not thread-safe) gc_mode GarbageCollectionMode // Garbage collection mode (-gc flag) backend Backend - arch Arch = .auto + arch Arch = .auto + target_os string = os.user_os() output_file string printfn_list []string // List of function names whose generated C source should be printed user_defines []string // User-defined comptime flags via -d @@ -167,6 +168,13 @@ pub fn new_preferences() Preferences { } } +pub fn (p &Preferences) target_os_or_host() string { + if p == unsafe { nil } || p.target_os == '' { + return os.user_os() + } + return p.target_os +} + // new_preferences_from_args parses full args list including option values pub fn new_preferences_from_args(args []string) Preferences { // Default backend is cleanc @@ -431,5 +439,9 @@ pub fn (p &Preferences) get_effective_arch() Arch { return p.arch } // Auto-detect: macOS defaults to arm64, others to x64 - return if os.user_os() == 'macos' { Arch.arm64 } else { Arch.x64 } + return if normalize_current_os_name(p.target_os_or_host()) == 'macos' { + Arch.arm64 + } else { + Arch.x64 + } } diff --git a/vlib/v2/ssa/builder.v b/vlib/v2/ssa/builder.v index 6a7f361f0..ba13e2faa 100644 --- a/vlib/v2/ssa/builder.v +++ b/vlib/v2/ssa/builder.v @@ -27,6 +27,17 @@ fn enum_field_symbol_name(name string) string { return escaped } +fn normalize_target_os_name(target_os string) string { + return match target_os.to_lower() { + 'macos', 'darwin' { 'macos' } + else { target_os.to_lower() } + } +} + +fn (b &Builder) is_macos_target() bool { + return normalize_target_os_name(b.target_os) == 'macos' +} + fn ssa_module_storage_name(module_name string, name string) string { if name == '' || name.starts_with('C.') { return name @@ -72,6 +83,11 @@ pub mut: // Native self-hosted builds use the SSA sumtype layout directly; guard // against null large-variant payloads before matching types.Type. guard_invalid_type_payloads bool + // Target OS for lowering target-specific C globals. + target_os string + // When set, native Windows PE builds rely on markused instead of retaining + // broad builtin runtime modules before linking. + minimal_runtime_roots bool mut: env &types.Environment = unsafe { nil } cur_func int = -1 @@ -167,6 +183,8 @@ pub fn (mut b Builder) new_worker_clone(worker_mod &Module, worker_idx int) &Bui return &Builder{ mod: worker_mod env: b.env + target_os: b.target_os + minimal_runtime_roots: b.minimal_runtime_roots struct_types: b.struct_types.clone() enum_values: b.enum_values.clone() fn_index: b.fn_index.clone() @@ -271,7 +289,7 @@ pub fn (mut b Builder) build_all(files []ast.File) { } // Phase 3.5: Generate synthetic stubs for transformer-generated functions - if b.hot_fn.len == 0 { + if b.hot_fn.len == 0 && !b.minimal_runtime_roots { b.generate_array_eq_stub() b.generate_wymix_stub() b.generate_wyhash64_stub() @@ -2625,18 +2643,21 @@ pub fn (mut b Builder) should_build_fn(file_name string, decl ast.FnDecl) bool { if b.used_fn_keys.len == 0 { return true // No markused data — build everything } - // Always build init_consts, init, deinit, main - if decl.name.starts_with('__v_init_consts_') { - return true - } - if decl.name == 'init' || decl.name == 'deinit' { + if decl.name == 'main' { return true } - if decl.name == 'main' { + // Always build init_consts, init, deinit unless a minimal native runtime path + // relies on markused to keep only roots that are actually reached. + if decl.name.starts_with('__v_init_consts_') || decl.name == 'init' || decl.name == 'deinit' { + if b.minimal_runtime_roots { + key := markused.decl_key(b.cur_module, decl, b.env) + return key in b.used_fn_keys + } return true } // Always build functions from core modules that the runtime needs - if b.cur_module in ['builtin', 'strings', 'strconv', 'bits', 'sha256', 'binary'] { + if !b.minimal_runtime_roots + && b.cur_module in ['builtin', 'strings', 'strconv', 'bits', 'sha256', 'binary'] { return true } // Always build .vh header declarations @@ -2646,7 +2667,7 @@ pub fn (mut b Builder) should_build_fn(file_name string, decl ast.FnDecl) bool { // Keep transformer-generated array/map method specializations if decl.is_method { mangled := b.mangle_fn_name(decl) - if mangled.contains('__Array_') || mangled.contains('__Map_') { + if !b.minimal_runtime_roots && (mangled.contains('__Array_') || mangled.contains('__Map_')) { return true } } @@ -6429,35 +6450,41 @@ fn (mut b Builder) build_selector(expr ast.SelectorExpr) ValueID { } // macOS errno: (*__error()) — call __error() which returns int* if c_name == 'errno' { - i32_t := b.mod.type_store.get_int(32) - ptr_i32 := b.mod.type_store.get_ptr(i32_t) - err_fn := b.get_or_create_fn_ref('__error', ptr_i32) - call_val := b.mod.add_instr(.call, b.cur_block, ptr_i32, [err_fn]) - return b.mod.add_instr(.load, b.cur_block, i32_t, [call_val]) + if b.is_macos_target() { + i32_t := b.mod.type_store.get_int(32) + ptr_i32 := b.mod.type_store.get_ptr(i32_t) + err_fn := b.get_or_create_fn_ref('__error', ptr_i32) + call_val := b.mod.add_instr(.call, b.cur_block, ptr_i32, [err_fn]) + return b.mod.add_instr(.load, b.cur_block, i32_t, [call_val]) + } } - // Map C standard I/O streams to macOS-specific symbol names - macos_name := match c_name { - 'stdout' { '__stdoutp' } - 'stderr' { '__stderrp' } - 'stdin' { '__stdinp' } - else { c_name } + // Map C standard I/O streams to macOS-specific symbol names only for macOS. + target_name := if b.is_macos_target() { + match c_name { + 'stdout' { '__stdoutp' } + 'stderr' { '__stderrp' } + 'stdin' { '__stdinp' } + else { c_name } + } + } else { + c_name } i8_t := b.mod.type_store.get_int(8) ptr_t := b.mod.type_store.get_ptr(i8_t) - if c_name in ['stdout', 'stderr', 'stdin'] { + if b.is_macos_target() && c_name in ['stdout', 'stderr', 'stdin'] { // `__stdoutp` / `__stdinp` / `__stderrp` are libSystem `FILE*` variables. // Reading the V expression `C.stdout` must yield the FILE* value, not the // address of the variable. The external global is pre-registered on the // main module (build_all) so worker modules see it via seeded values. - glob := b.mod.add_external_global(macos_name, ptr_t) - b.global_refs[macos_name] = glob + glob := b.mod.add_external_global(target_name, ptr_t) + b.global_refs[target_name] = glob return b.mod.add_instr(.load, b.cur_block, ptr_t, [glob]) } // Not a known constant — emit as a global reference (e.g. C.stdout, C.stderr) - glob := b.mod.add_value_node(.global, ptr_t, macos_name, 0) - b.global_refs[macos_name] = glob + glob := b.mod.add_value_node(.global, ptr_t, target_name, 0) + b.global_refs[target_name] = glob return glob } diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index fc3f5a422..47ecebe24 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -1785,6 +1785,113 @@ fn module_init_call_name(mod string) string { return 'init' } +fn (t &Transformer) uses_minimal_windows_x64_runtime() bool { + return t.pref != unsafe { nil } && t.pref.backend == .x64 && t.pref.get_effective_arch() == .x64 + && t.pref.target_os_or_host().to_lower() == 'windows' +} + +fn (t &Transformer) collect_runtime_init_imports_from_if_expr(node ast.IfExpr, mut imports []ast.ImportStmt) { + if t.eval_comptime_cond(node.cond) { + t.collect_runtime_init_imports_from_stmts(node.stmts, mut imports) + return + } + match node.else_expr { + ast.IfExpr { + if node.else_expr.cond is ast.EmptyExpr { + t.collect_runtime_init_imports_from_stmts(node.else_expr.stmts, mut imports) + } else { + t.collect_runtime_init_imports_from_if_expr(node.else_expr, mut imports) + } + } + else {} + } +} + +fn (t &Transformer) collect_runtime_init_imports_from_stmts(stmts []ast.Stmt, mut imports []ast.ImportStmt) { + for stmt in stmts { + match stmt { + ast.ImportStmt { + imports << stmt + } + ast.ExprStmt { + if stmt.expr is ast.ComptimeExpr && stmt.expr.expr is ast.IfExpr { + t.collect_runtime_init_imports_from_if_expr(stmt.expr.expr, mut imports) + } + } + else {} + } + } +} + +fn (t &Transformer) active_runtime_init_imports(file ast.File) []ast.ImportStmt { + mut imports := file.imports.clone() + t.collect_runtime_init_imports_from_stmts(file.stmts, mut imports) + return imports +} + +fn runtime_init_available_modules(files []ast.File) map[string]bool { + mut available := map[string]bool{} + available[''] = true + available['main'] = true + for file in files { + if file.mod != '' { + available[file.mod] = true + } + } + return available +} + +fn add_runtime_init_module_key(mut modules map[string]bool, name string, available map[string]bool) bool { + if name == '' { + return false + } + mut changed := false + mut matched_available := false + mut candidates := [name] + if name.contains('.') { + short_name := name.all_after_last('.') + if short_name != '' { + candidates << short_name + } + } + for candidate in candidates { + if candidate !in available { + continue + } + matched_available = true + if candidate !in modules { + modules[candidate] = true + changed = true + } + } + if !matched_available && name !in modules { + modules[name] = true + changed = true + } + return changed +} + +fn (t &Transformer) imported_runtime_init_modules(files []ast.File) map[string]bool { + mut modules := map[string]bool{} + modules[''] = true + available_modules := runtime_init_available_modules(files) + add_runtime_init_module_key(mut modules, 'main', available_modules) + mut changed := true + for changed { + changed = false + for file in files { + if file.mod !in modules { + continue + } + for imp in t.active_runtime_init_imports(file) { + changed = add_runtime_init_module_key(mut modules, imp.name, available_modules) + || changed + } + } + } + return modules +} + fn is_builtin_main_arg_global(mod string, name string) bool { return mod == 'builtin' && (name == 'g_main_argc' || name == 'g_main_argv') } @@ -2578,10 +2685,19 @@ fn (mut t Transformer) inject_test_main(mut files []ast.File) { fn (mut t Transformer) inject_main_runtime_const_init_calls(mut files []ast.File) { mut init_calls := []ast.Stmt{} mut seen_init_mods := map[string]bool{} + minimal_windows_x64_runtime := t.uses_minimal_windows_x64_runtime() + init_modules := if minimal_windows_x64_runtime { + t.imported_runtime_init_modules(files) + } else { + map[string]bool{} + } for file in files { if file.mod in seen_init_mods { continue } + if minimal_windows_x64_runtime && file.mod !in init_modules { + continue + } for stmt in file.stmts { if stmt is ast.FnDecl && !stmt.is_method && stmt.name == 'init' { seen_init_mods[file.mod] = true @@ -2597,6 +2713,9 @@ fn (mut t Transformer) inject_main_runtime_const_init_calls(mut files []ast.File } } for mod in t.runtime_const_modules { + if minimal_windows_x64_runtime && mod !in init_modules { + continue + } fn_name := t.runtime_const_init_fn_name[mod] or { continue } call_name := runtime_const_init_call_name(mod, fn_name) init_calls << ast.ExprStmt{ diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 1f28f4289..85143e0ed 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -4914,105 +4914,7 @@ fn (c &Checker) eval_comptime_cond(cond ast.Expr) bool { // eval_comptime_flag evaluates a single comptime flag/identifier fn (c &Checker) eval_comptime_flag(name string) bool { - match name { - 'macos', 'darwin' { - $if macos { - return true - } - return false - } - 'linux' { - $if linux { - return true - } - return false - } - 'windows' { - $if windows { - return true - } - return false - } - 'bsd' { - $if macos || freebsd || openbsd || netbsd || dragonfly { - return true - } - return false - } - 'freebsd' { - $if freebsd { - return true - } - return false - } - 'openbsd' { - $if openbsd { - return true - } - return false - } - 'netbsd' { - $if netbsd { - return true - } - return false - } - 'dragonfly' { - $if dragonfly { - return true - } - return false - } - 'x64', 'amd64' { - $if amd64 { - return true - } - return false - } - 'arm64', 'aarch64' { - $if arm64 { - return true - } - return false - } - 'little_endian' { - $if little_endian { - return true - } - return false - } - 'big_endian' { - $if big_endian { - return true - } - return false - } - 'debug' { - $if debug { - return true - } - return false - } - 'native' { - return c.pref != unsafe { nil } && (c.pref.backend == .arm64 || c.pref.backend == .x64) - } - 'builtin_write_buf_to_fd_should_use_c_write' { - return c.pref != unsafe { nil } && (c.pref.backend == .arm64 || c.pref.backend == .x64) - } - 'prealloc' { - return c.pref != unsafe { nil } && c.pref.prealloc - } - 'new_int', 'gcboehm', 'autofree', 'ppc64' { - return false - } - else { - // Check user-defined comptime flags from -d - if c.pref != unsafe { nil } && name in c.pref.user_defines { - return true - } - return false - } - } + return pref.comptime_flag_value(c.pref, name) } fn (mut c Checker) comptime_stmt_list_type(stmts []ast.Stmt) Type { -- 2.39.5