commit 2322d45d80a88730b356b6a2b0101bcffb5a0afd (tree)
parent 07f05426fc0c28ad19f87002d5d4d0366e54b7c9
Author: Andrew Kelley <andrew@ziglang.org>
Date: Sun, 12 Apr 2026 01:34:16 +0200
Merge pull request 'Implement variadic functions for Win64 in the x86_64 backend' (#31672) from kcbanner/zig:win64_varargs into master
Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31672
Reviewed-by: Andrew Kelley <andrew@ziglang.org>
Diffstat:
5 files changed, 228 insertions(+), 44 deletions(-)
diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig
@@ -219,6 +219,8 @@ generated_docs: ?*GeneratedFile,
generated_asm: ?*GeneratedFile,
generated_bin: ?*GeneratedFile,
generated_pdb: ?*GeneratedFile,
+// hack for stage2_x86_64 + coff
+generated_compiler_rt_dyn_lib: ?*GeneratedFile,
generated_implib: ?*GeneratedFile,
generated_llvm_bc: ?*GeneratedFile,
generated_llvm_ir: ?*GeneratedFile,
@@ -441,6 +443,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
.generated_asm = null,
.generated_bin = null,
.generated_pdb = null,
+ .generated_compiler_rt_dyn_lib = null,
.generated_implib = null,
.generated_llvm_bc = null,
.generated_llvm_ir = null,
@@ -691,6 +694,13 @@ pub fn producesPdbFile(compile: *Compile) bool {
return compile.isDynamicLibrary() or compile.kind == .exe or compile.kind == .@"test";
}
+pub fn producesCompilerRtDynLib(compile: *Compile) bool {
+ if (compile.rootModuleTarget().ofmt != .coff) return false;
+ if (compile.bundle_compiler_rt orelse (compile.kind == .exe or compile.isDynamicLibrary()))
+ return compile.use_llvm == false;
+ return false;
+}
+
pub fn producesImplib(compile: *Compile) bool {
return compile.isDll();
}
@@ -869,6 +879,12 @@ pub fn getEmittedPdb(compile: *Compile) LazyPath {
return compile.getEmittedFileGeneric(&compile.generated_pdb);
}
+/// Returns the generated compiler_rt dynamic library.
+/// This is a hack for stage2_x86_64 + coff.
+pub fn getEmittedCompilerRtDynLib(compile: *Compile) ?LazyPath {
+ return compile.getEmittedFileGeneric(&compile.generated_compiler_rt_dyn_lib);
+}
+
/// Returns the path to the generated documentation directory.
pub fn getEmittedDocs(compile: *Compile) LazyPath {
return compile.getEmittedFileGeneric(&compile.generated_docs);
@@ -1794,6 +1810,8 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
// zig fmt: off
if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin);
if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb);
+ // hack for stage2_x86_64 + coff
+ if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib);
if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib);
if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h);
if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs);
diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig
@@ -17,6 +17,10 @@ emitted_implib: ?LazyPath,
pdb_dir: ?InstallDir,
emitted_pdb: ?LazyPath,
+// hack for stage2_x86_64 + coff
+compiler_rt_dyn_lib_dir: ?InstallDir,
+emitted_compiler_rt_dyn_lib: ?LazyPath,
+
h_dir: ?InstallDir,
emitted_h: ?LazyPath,
@@ -35,6 +39,7 @@ pub const Options = struct {
/// Which installation directory to put the main output file into.
dest_dir: Dir = .default,
pdb_dir: Dir = .default,
+ compiler_rt_dyn_lib_dir: Dir = .default,
h_dir: Dir = .default,
implib_dir: Dir = .default,
@@ -75,6 +80,11 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
.default => if (artifact.producesPdbFile()) dest_dir else null,
.override => |o| o,
},
+ .compiler_rt_dyn_lib_dir = switch (options.compiler_rt_dyn_lib_dir) {
+ .disabled => null,
+ .default => if (artifact.producesCompilerRtDynLib()) dest_dir else null,
+ .override => |o| o,
+ },
.h_dir = switch (options.h_dir) {
.disabled => null,
.default => if (artifact.kind == .lib) .header else null,
@@ -98,6 +108,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
.emitted_bin = null,
.emitted_pdb = null,
+ .emitted_compiler_rt_dyn_lib = null,
.emitted_h = null,
.emitted_implib = null,
@@ -107,6 +118,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
install_artifact.step.dependOn(&artifact.step);
if (install_artifact.dest_dir != null) install_artifact.emitted_bin = artifact.getEmittedBin();
+ if (install_artifact.compiler_rt_dyn_lib_dir != null) install_artifact.emitted_compiler_rt_dyn_lib = artifact.getEmittedCompilerRtDynLib();
if (install_artifact.pdb_dir != null) install_artifact.emitted_pdb = artifact.getEmittedPdb();
// https://github.com/ziglang/zig/issues/9698
//if (install_artifact.h_dir != null) install_artifact.emitted_h = artifact.getEmittedH();
@@ -135,6 +147,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
install_artifact.artifact.installed_path = full_dest_path;
}
+ if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| {
+ const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step));
+ const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path);
+ all_cached = all_cached and p == .fresh;
+ }
+
if (install_artifact.implib_dir) |implib_dir| {
const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step));
const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path);
diff --git a/lib/std/zig.zig b/lib/std/zig.zig
@@ -986,11 +986,14 @@ pub const EmitArtifact = enum {
docs,
pdb,
h,
+ compiler_rt_dyn_lib,
/// If using `Server` to communicate with the compiler, it will place requested artifacts in
/// paths under the output directory, where those paths are named according to this function.
/// Returned string is allocated with `gpa` and owned by the caller.
pub fn cacheName(ea: EmitArtifact, gpa: Allocator, opts: BinNameOptions) Allocator.Error![]const u8 {
+ // hack for stage2_x86_64 + coff. See Coff.flush.
+ if (ea == .compiler_rt_dyn_lib) return "compiler_rt.dll";
const suffix: []const u8 = switch (ea) {
.bin => return binNameAlloc(gpa, opts),
.@"asm" => ".s",
@@ -1000,6 +1003,7 @@ pub const EmitArtifact = enum {
.docs => "-docs",
.pdb => ".pdb",
.h => ".h",
+ .compiler_rt_dyn_lib => unreachable,
};
return std.fmt.allocPrint(gpa, "{s}{s}", .{ opts.root_name, suffix });
}
diff --git a/src/codegen/x86_64/CodeGen.zig b/src/codegen/x86_64/CodeGen.zig
@@ -2049,7 +2049,16 @@ fn gen(
self.performReloc(skip_sse_reloc);
},
- .x86_64_win => return self.fail("TODO implement gen var arg function for Win64", .{}),
+ .x86_64_win => {
+ for (abi.Win64.c_abi_int_param_regs[0..], 0..) |reg, reg_i|
+ try self.genSetMem(
+ .{ .frame = .args_frame },
+ @intCast(reg_i * 8),
+ .usize,
+ .{ .register = reg },
+ .{},
+ );
+ },
else => |cc| return self.fail("{s} does not support var args", .{@tagName(cc)}),
};
@@ -176020,12 +176029,22 @@ fn genCall(self: *CodeGen, info: union(enum) {
.indirect => |reg_off| try self.register_manager.getReg(reg_off.reg, null),
else => unreachable,
}
- for (call_info.args, arg_types, args, frame_indices) |dst_arg, arg_ty, src_arg, *frame_index|
+ for (call_info.args, arg_types, args, frame_indices, 0..) |dst_arg, arg_ty, src_arg, *frame_index, arg_i|
switch (dst_arg) {
.none => {},
.register => |reg| {
try self.register_manager.getReg(reg, null);
try reg_locks.append(self.register_manager.lockReg(reg));
+
+ if (fn_info.is_var_args and
+ fn_info.cc == .x86_64_win and
+ reg.class() == .sse and
+ arg_i < abi.Win64.c_abi_int_param_regs.len)
+ {
+ // Floating point arguments must be duplicated into the equivalent integer registers on this ABI
+ const int_reg = abi.Win64.c_abi_int_param_regs[arg_i];
+ try reg_locks.append(self.register_manager.lockReg(int_reg));
+ }
},
.register_pair => |regs| {
for (regs) |reg| try self.register_manager.getReg(reg, null);
@@ -176129,7 +176148,7 @@ fn genCall(self: *CodeGen, info: union(enum) {
else => unreachable,
}
- for (call_info.args, arg_types, args, frame_indices) |dst_arg, arg_ty, src_arg, frame_index|
+ for (call_info.args, arg_types, args, frame_indices, 0..) |dst_arg, arg_ty, src_arg, frame_index, arg_i|
switch (dst_arg) {
.none, .load_frame => {},
.register => |dst_reg| switch (fn_info.cc) {
@@ -176144,6 +176163,16 @@ fn genCall(self: *CodeGen, info: union(enum) {
try self.genSetReg(dst_alias, promoted_ty, src_arg, opts);
if (promoted_ty.toIntern() != arg_ty.toIntern())
try self.truncateRegister(arg_ty, dst_alias);
+
+ if (fn_info.is_var_args and
+ fn_info.cc == .x86_64_win and
+ dst_reg.class() == .sse and
+ arg_i < abi.Win64.c_abi_int_param_regs.len)
+ {
+ const int_dst_reg = abi.Win64.c_abi_int_param_regs[arg_i];
+ const int_dst_alias = registerAlias(int_dst_reg, promoted_abi_size);
+ try self.genSetReg(int_dst_alias, promoted_ty, .{ .register = dst_alias }, opts);
+ }
},
},
.register_pair => try self.genCopy(arg_ty, dst_arg, src_arg, opts),
@@ -176180,7 +176209,8 @@ fn genCall(self: *CodeGen, info: union(enum) {
else => unreachable,
};
- if (fn_info.is_var_args) try self.asmRegisterImmediate(.{ ._, .mov }, .al, .u(call_info.fp_count));
+ if (fn_info.is_var_args and fn_info.cc == .x86_64_sysv)
+ try self.asmRegisterImmediate(.{ ._, .mov }, .al, .u(call_info.fp_count));
// Due to incremental compilation, how function calls are generated depends
// on linking.
@@ -180792,7 +180822,18 @@ fn airVaStart(self: *CodeGen, inst: Air.Inst.Index) !void {
field_off += @intCast(ptr_anyopaque_ty.abiSize(zcu));
break :result .{ .load_frame = .{ .index = dst_fi } };
},
- .x86_64_win => return self.fail("TODO implement c_va_start for Win64", .{}),
+ .x86_64_win => result: {
+ const dst_fi = try self.allocFrameIndex(.initSpill(va_list_ty, zcu));
+ const fn_info = zcu.typeToFunc(self.fn_type).?;
+ try self.genSetMem(
+ .{ .frame = dst_fi },
+ 0,
+ ptr_anyopaque_ty,
+ .{ .lea_frame = .{ .index = .args_frame, .off = @intCast(fn_info.param_types.len * 8) } },
+ .{},
+ );
+ break :result .{ .load_frame = .{ .index = dst_fi } };
+ },
else => |cc| return self.fail("{s} does not support var args", .{@tagName(cc)}),
};
return self.finishAir(inst, result, .{ .none, .none, .none });
@@ -180931,48 +180972,103 @@ fn airVaArg(self: *CodeGen, inst: Air.Inst.Index) !void {
break :result dst_mcv;
}
- assert(ty.toIntern() == .f32_type and promote_ty.toIntern() == .f64_type);
- const dst_mcv = if (promote_mcv.isRegister())
- promote_mcv
- else
- try self.copyToRegisterWithInstTracking(inst, ty, promote_mcv);
- const dst_reg = dst_mcv.getReg().?.to128();
- const dst_lock = self.register_manager.lockReg(dst_reg);
- defer if (dst_lock) |lock| self.register_manager.unlockReg(lock);
+ try self.convertFloatVarArg(inst, ty, promote_ty, promote_mcv);
+ break :result promote_mcv;
+ },
+ .x86_64_win => result: {
+ try self.spillEflagsIfOccupied();
- if (self.hasFeature(.avx)) if (promote_mcv.isBase()) try self.asmRegisterRegisterMemory(
- .{ .v_ss, .cvtsd2 },
- dst_reg,
- dst_reg,
- try promote_mcv.mem(self, .{ .size = .qword }),
- ) else try self.asmRegisterRegisterRegister(
- .{ .v_ss, .cvtsd2 },
- dst_reg,
- dst_reg,
- (if (promote_mcv.isRegister())
- promote_mcv.getReg().?
- else
- try self.copyToTmpRegister(promote_ty, promote_mcv)).to128(),
- ) else if (promote_mcv.isBase()) try self.asmRegisterMemory(
- .{ ._ss, .cvtsd2 },
- dst_reg,
- try promote_mcv.mem(self, .{ .size = .qword }),
- ) else try self.asmRegisterRegister(
- .{ ._ss, .cvtsd2 },
- dst_reg,
- (if (promote_mcv.isRegister())
- promote_mcv.getReg().?
- else
- try self.copyToTmpRegister(promote_ty, promote_mcv)).to128(),
- );
+ const promote_mcv = try self.allocTempRegOrMem(promote_ty, true);
+ const promote_lock = switch (promote_mcv) {
+ .register => |reg| self.register_manager.lockRegAssumeUnused(reg),
+ else => null,
+ };
+ defer if (promote_lock) |lock| self.register_manager.unlockReg(lock);
+
+ const ptr_arg_list_reg =
+ try self.copyToTmpRegister(self.typeOf(ty_op.operand), .{ .air_ref = ty_op.operand });
+ const ptr_arg_list_lock = self.register_manager.lockRegAssumeUnused(ptr_arg_list_reg);
+ defer self.register_manager.unlockReg(ptr_arg_list_lock);
+
+ const next_arg_ptr: MCValue = .{ .indirect = .{ .reg = ptr_arg_list_reg } };
+
+ const class = abi.classifyWindows(promote_ty, zcu, self.target, .arg);
+ switch (class) {
+ .integer, .sse => {
+ const next_arg_ptr_reg = try self.copyToTmpRegister(.usize, next_arg_ptr);
+ if (!unused) try self.genCopy(promote_ty, promote_mcv, .{
+ .indirect = .{ .reg = next_arg_ptr_reg },
+ }, .{});
+ try self.asmRegisterMemory(.{ ._, .lea }, next_arg_ptr_reg, .{
+ .base = .{ .reg = next_arg_ptr_reg.to64() },
+ .mod = .{ .rm = .{ .disp = 8 } },
+ });
+ try self.genCopy(.usize, next_arg_ptr, .{ .register = next_arg_ptr_reg }, .{});
+ },
+ .memory => unreachable,
+ else => return self.fail("TODO implement c_va_arg for {f} on Win64", .{promote_ty.fmt(pt)}),
+ }
+
+ if (unused) break :result .unreach;
+ if (ty.toIntern() == promote_ty.toIntern()) break :result promote_mcv;
+
+ if (!promote_ty.isRuntimeFloat()) {
+ const dst_mcv = try self.allocRegOrMem(inst, true);
+ try self.genCopy(ty, dst_mcv, promote_mcv, .{});
+ break :result dst_mcv;
+ }
+
+ try self.convertFloatVarArg(inst, ty, promote_ty, promote_mcv);
break :result promote_mcv;
},
- .x86_64_win => return self.fail("TODO implement c_va_arg for Win64", .{}),
else => |cc| return self.fail("{s} does not support var args", .{@tagName(cc)}),
};
return self.finishAir(inst, result, .{ ty_op.operand, .none, .none });
}
+fn convertFloatVarArg(
+ self: *CodeGen,
+ inst: Air.Inst.Index,
+ ty: Type,
+ promote_ty: Type,
+ promote_mcv: MCValue,
+) !void {
+ assert(ty.toIntern() == .f32_type and promote_ty.toIntern() == .f64_type);
+ const dst_mcv = if (promote_mcv.isRegister())
+ promote_mcv
+ else
+ try self.copyToRegisterWithInstTracking(inst, ty, promote_mcv);
+ const dst_reg = dst_mcv.getReg().?.to128();
+ const dst_lock = self.register_manager.lockReg(dst_reg);
+ defer if (dst_lock) |lock| self.register_manager.unlockReg(lock);
+
+ if (self.hasFeature(.avx)) if (promote_mcv.isBase()) try self.asmRegisterRegisterMemory(
+ .{ .v_ss, .cvtsd2 },
+ dst_reg,
+ dst_reg,
+ try promote_mcv.mem(self, .{ .size = .qword }),
+ ) else try self.asmRegisterRegisterRegister(
+ .{ .v_ss, .cvtsd2 },
+ dst_reg,
+ dst_reg,
+ (if (promote_mcv.isRegister())
+ promote_mcv.getReg().?
+ else
+ try self.copyToTmpRegister(promote_ty, promote_mcv)).to128(),
+ ) else if (promote_mcv.isBase()) try self.asmRegisterMemory(
+ .{ ._ss, .cvtsd2 },
+ dst_reg,
+ try promote_mcv.mem(self, .{ .size = .qword }),
+ ) else try self.asmRegisterRegister(
+ .{ ._ss, .cvtsd2 },
+ dst_reg,
+ (if (promote_mcv.isRegister())
+ promote_mcv.getReg().?
+ else
+ try self.copyToTmpRegister(promote_ty, promote_mcv)).to128(),
+ );
+}
+
fn airVaCopy(self: *CodeGen, inst: Air.Inst.Index) !void {
const ty_op = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_op;
const ptr_va_list_ty = self.typeOf(ty_op.operand);
diff --git a/test/behavior/var_args.zig b/test/behavior/var_args.zig
@@ -101,7 +101,7 @@ test "simple variadic function" {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
- if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) return error.SkipZigTest; // TODO
+ if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // TODO
if (builtin.cpu.arch == .s390x and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21350
if (builtin.cpu.arch.isSPARC() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23718
if (builtin.cpu.arch.isRISCV() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/25064
@@ -163,7 +163,7 @@ test "coerce reference to var arg" {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
- if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) return error.SkipZigTest; // TODO
+ if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // TODO
if (builtin.cpu.arch == .s390x and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21350
const S = struct {
@@ -195,7 +195,7 @@ test "variadic functions" {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
- if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) return error.SkipZigTest; // TODO
+ if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // TODO
if (builtin.cpu.arch == .s390x and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21350
if (builtin.cpu.arch.isSPARC() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23718
if (builtin.cpu.arch.isRISCV() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/25064
@@ -248,7 +248,7 @@ test "copy VaList" {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
- if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) return error.SkipZigTest; // TODO
+ if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // TODO
if (builtin.cpu.arch == .s390x and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/21350
if (builtin.cpu.arch.isSPARC() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23718
if (builtin.cpu.arch.isRISCV() and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/25064
@@ -283,7 +283,7 @@ test "unused VaList arg" {
// https://github.com/ziglang/zig/issues/14096
return error.SkipZigTest;
}
- if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) {
+ if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows and builtin.zig_backend == .stage2_llvm) {
// https://github.com/ziglang/zig/issues/16961
return error.SkipZigTest; // TODO
}
@@ -305,3 +305,51 @@ test "unused VaList arg" {
const x = S.thirdArg(0, @as(c_int, 1), @as(c_int, 2));
try std.testing.expectEqual(@as(c_int, 2), x);
}
+
+test "floating point VaList args" {
+ if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+ if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+ if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
+ if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
+ if (builtin.zig_backend == .stage2_llvm) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/16961
+
+ // Float register arguments are handled specially on cc == .x86_64_win, so it's important that we test all 4 slots,
+ // and pre-C23 doesn't allow a variadic function without at least one non-variadic argument.
+ if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
+
+ const S = struct {
+ fn proxy(...) callconv(.c) void {
+ var ap = @cVaStart();
+ defer @cVaEnd(&ap);
+
+ var out_f32: [3]f32 = undefined;
+ var out_f64: [3]f64 = undefined;
+ out_f32[0] = @cVaArg(&ap, f32);
+ out_f64[0] = @cVaArg(&ap, f64);
+ out_f32[1] = @cVaArg(&ap, f32);
+ out_f64[1] = @cVaArg(&ap, f64);
+ out_f32[2] = @cVaArg(&ap, f32);
+ out_f64[2] = @cVaArg(&ap, f64);
+ @cVaArg(&ap, *[3]f32).* = out_f32;
+ @cVaArg(&ap, *[3]f64).* = out_f64;
+ }
+ };
+
+ const expected_f32: []const f32 = &.{ 1000, std.math.floatMax(f32), std.math.floatMin(f32) };
+ const expected_f64: []const f64 = &.{ 2000, std.math.floatMax(f64), std.math.floatMin(f64) };
+ var actual_f32: [3]f32 = undefined;
+ var actual_f64: [3]f64 = undefined;
+ S.proxy(
+ expected_f32[0],
+ expected_f64[0],
+ expected_f32[1],
+ expected_f64[1],
+ expected_f32[2],
+ expected_f64[2],
+ &actual_f32,
+ &actual_f64,
+ );
+
+ try std.testing.expectEqualSlices(f32, expected_f32, &actual_f32);
+ try std.testing.expectEqualSlices(f64, expected_f64, &actual_f64);
+}