ffi #foreign: C-variadic tail via args: ..T
Trailing `args: ..T` on a #foreign declaration now lowers to the C calling convention's `...` instead of sx-side slice-packing. Drops the per-arity #foreign-shim workaround for callers of variadic C APIs (__android_log_print, printf-family, etc.). Closes issue-0043. - IR: Function.is_variadic on inst.Function; declareFunction drops the variadic param from the IR signature for foreign+variadic decls. - emit_llvm: LLVMFunctionType receives is_var_arg=1 when the flag is set; call lowering passes extras through unchanged. - Lowering: packVariadicCallArgs early-outs for foreign+variadic (no slice-pack); new promoteCVariadicArgs applies C default argument promotion (bool/s8/s16/u8/u16 -> s32, f32 -> f64) to extras past the fixed param count. - Test: examples/ffi-foreign-cvariadic.sx + .c exercise s64/f64/s32 returns through C va_arg over s32/f64/*u8 element types. 134 host + 6 cross tests pass on the WIP-less baseline.
This commit is contained in:
30
examples/ffi-foreign-cvariadic.c
Normal file
30
examples/ffi-foreign-cvariadic.c
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <stdarg.h>
|
||||
|
||||
long long sx_ffi_sum_ints(int n, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, n);
|
||||
long long total = 0;
|
||||
for (int i = 0; i < n; i++) total += va_arg(ap, int);
|
||||
va_end(ap);
|
||||
return total;
|
||||
}
|
||||
|
||||
double sx_ffi_avg_doubles(int n, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, n);
|
||||
double total = 0.0;
|
||||
for (int i = 0; i < n; i++) total += va_arg(ap, double);
|
||||
va_end(ap);
|
||||
if (n == 0) return 0.0;
|
||||
return total / n;
|
||||
}
|
||||
|
||||
int sx_ffi_count_args(const char *tag, ...) {
|
||||
(void) tag;
|
||||
va_list ap;
|
||||
va_start(ap, tag);
|
||||
int count = 0;
|
||||
while (va_arg(ap, const char *) != 0) count++;
|
||||
va_end(ap);
|
||||
return count;
|
||||
}
|
||||
28
examples/ffi-foreign-cvariadic.sx
Normal file
28
examples/ffi-foreign-cvariadic.sx
Normal file
@@ -0,0 +1,28 @@
|
||||
// `#foreign` C-variadic tail: trailing `args: ..T` on a foreign fn maps
|
||||
// to the C calling convention's `...`. Extras at the call site are
|
||||
// passed via the variadic slot with the standard default argument
|
||||
// promotion (s8/s16/bool → s32, f32 → f64) applied implicitly.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#source "ffi-foreign-cvariadic.c";
|
||||
};
|
||||
|
||||
sx_ffi_sum_ints :: (n: s32, args: ..s32) -> s64 #foreign;
|
||||
sx_ffi_avg_doubles :: (n: s32, args: ..f64) -> f64 #foreign;
|
||||
sx_ffi_count_args :: (tag: *u8, args: ..*u8) -> s32 #foreign;
|
||||
|
||||
main :: () -> s32 {
|
||||
print("sum_ints(3, 10, 20, 30) = {}\n", sx_ffi_sum_ints(3, 10, 20, 30));
|
||||
print("sum_ints(0) = {}\n", sx_ffi_sum_ints(0));
|
||||
print("avg_doubles(2) = {}\n", sx_ffi_avg_doubles(2, 1.5, 2.5));
|
||||
print("avg_doubles(3) = {}\n", sx_ffi_avg_doubles(3, 1.0, 2.0, 3.0));
|
||||
|
||||
a := "alpha".ptr;
|
||||
b := "beta".ptr;
|
||||
g := "gamma".ptr;
|
||||
sentinel : *u8 = null;
|
||||
print("count_args(3 strs) = {}\n", sx_ffi_count_args("tag".ptr, a, b, g, sentinel));
|
||||
0;
|
||||
}
|
||||
@@ -662,7 +662,8 @@ pub const LLVMEmitter = struct {
|
||||
param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty;
|
||||
}
|
||||
|
||||
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, 0);
|
||||
const is_var_arg: c_int = if (func.is_variadic) 1 else 0;
|
||||
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, is_var_arg);
|
||||
const name_z = self.alloc.dupeZ(u8, name) catch unreachable;
|
||||
defer self.alloc.free(name_z);
|
||||
|
||||
|
||||
@@ -478,6 +478,13 @@ pub const Function = struct {
|
||||
linkage: Linkage = .internal,
|
||||
call_conv: CallingConvention = .default,
|
||||
source_file: ?[]const u8 = null,
|
||||
/// Variadic tail at the IR signature level. Only `#foreign` decls reach
|
||||
/// IR with this set — sx-side `..T` params are slice-packed before
|
||||
/// lowering, so anything that survives is the C calling convention's
|
||||
/// `...`. emit_llvm passes `is_var_arg=1` to `LLVMFunctionType`; call
|
||||
/// sites apply the standard default argument promotions (s8/s16/bool →
|
||||
/// s32, f32 → f64) to extras past the fixed param count.
|
||||
is_variadic: bool = false,
|
||||
|
||||
pub const Param = struct {
|
||||
name: StringId,
|
||||
|
||||
@@ -671,8 +671,19 @@ pub const Lowering = struct {
|
||||
|
||||
const ret_ty = self.resolveReturnType(fd);
|
||||
|
||||
// Foreign declarations with a trailing variadic param map to the C
|
||||
// calling convention's `...` tail. Drop the variadic param from the
|
||||
// IR signature (it has no C-level slot) and set is_variadic.
|
||||
const is_foreign = fd.body.data == .foreign_expr;
|
||||
var is_variadic = false;
|
||||
var effective_params = fd.params;
|
||||
if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
||||
is_variadic = true;
|
||||
effective_params = fd.params[0 .. fd.params.len - 1];
|
||||
}
|
||||
|
||||
var params = std.ArrayList(Function.Param).empty;
|
||||
for (fd.params) |p| {
|
||||
for (effective_params) |p| {
|
||||
const pty = self.resolveParamType(&p);
|
||||
params.append(self.alloc, .{
|
||||
.name = self.module.types.internString(p.name),
|
||||
@@ -683,7 +694,7 @@ pub const Lowering = struct {
|
||||
const cc: Function.CallingConvention = if (fd.call_conv == .c) .c else .default;
|
||||
|
||||
// For #foreign with C name override, declare under C name and map sx name → C name
|
||||
if (fd.body.data == .foreign_expr) {
|
||||
if (is_foreign) {
|
||||
const fe = fd.body.data.foreign_expr;
|
||||
if (fe.c_name) |c_name| {
|
||||
const c_name_id = self.module.types.internString(c_name);
|
||||
@@ -691,6 +702,7 @@ pub const Lowering = struct {
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = cc;
|
||||
func.source_file = self.current_source_file;
|
||||
func.is_variadic = is_variadic;
|
||||
self.foreign_name_map.put(name, c_name) catch {};
|
||||
return;
|
||||
}
|
||||
@@ -701,6 +713,7 @@ pub const Lowering = struct {
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = cc;
|
||||
func.source_file = self.current_source_file;
|
||||
func.is_variadic = is_variadic;
|
||||
}
|
||||
|
||||
/// Check if a C-imported function is visible from the current source file.
|
||||
@@ -4651,6 +4664,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Coerce arguments to match parameter types
|
||||
self.coerceCallArgs(args.items, params);
|
||||
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
|
||||
return self.builder.call(fid, args.items, ret_ty);
|
||||
}
|
||||
}
|
||||
@@ -4859,6 +4873,7 @@ pub const Lowering = struct {
|
||||
self.packVariadicCallArgs(fd, c, &args);
|
||||
}
|
||||
self.coerceCallArgs(args.items, params);
|
||||
if (func.is_variadic) self.promoteCVariadicArgs(args.items, params.len);
|
||||
return self.builder.call(fid, args.items, ret_ty);
|
||||
}
|
||||
// Check if this is Type.variant(payload) — qualified enum construction
|
||||
@@ -6200,6 +6215,12 @@ pub const Lowering = struct {
|
||||
/// Detects variadic params in the function decl, packs remaining args into a typed slice,
|
||||
/// and replaces the args list with [fixed_args..., slice_ref].
|
||||
fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void {
|
||||
// `#foreign` variadic uses the C calling convention's `...` tail —
|
||||
// extras are passed through directly with default argument promotion
|
||||
// (handled at the call site), not packed into an sx slice.
|
||||
if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
||||
return;
|
||||
}
|
||||
// Find variadic param index
|
||||
var variadic_idx: ?usize = null;
|
||||
var elem_ty: TypeId = .any;
|
||||
@@ -10025,6 +10046,22 @@ pub const Lowering = struct {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Apply C default argument promotion to variadic-tail args. These rules
|
||||
/// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's
|
||||
/// implicit promotions when an argument is passed through `...`.
|
||||
fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void {
|
||||
if (args.len <= fixed_count) return;
|
||||
for (args[fixed_count..]) |*arg| {
|
||||
const src_ty = self.builder.getRefType(arg.*);
|
||||
const promoted: TypeId = switch (src_ty) {
|
||||
.bool, .s8, .s16, .u8, .u16 => .s32,
|
||||
.f32 => .f64,
|
||||
else => continue,
|
||||
};
|
||||
arg.* = self.coerceToType(arg.*, src_ty, promoted);
|
||||
}
|
||||
}
|
||||
|
||||
/// Coerce call arguments in-place to match function parameter types.
|
||||
fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void {
|
||||
for (0..@min(args.len, params.len)) |i| {
|
||||
|
||||
1
tests/expected/ffi-foreign-cvariadic.exit
Normal file
1
tests/expected/ffi-foreign-cvariadic.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
5
tests/expected/ffi-foreign-cvariadic.txt
Normal file
5
tests/expected/ffi-foreign-cvariadic.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
sum_ints(3, 10, 20, 30) = 60
|
||||
sum_ints(0) = 0
|
||||
avg_doubles(2) = 2.000000
|
||||
avg_doubles(3) = 2.000000
|
||||
count_args(3 strs) = 3
|
||||
Reference in New Issue
Block a user