如何在Swift中调用Variadic C函数?

mm9b1k5b  于 2022-11-21  发布在  Swift
关注(0)|答案(1)|浏览(168)

我需要帮助,我正在尝试调用Swift 5函数中的C函数。
我有一个无法解决的错误:
无法使用'evolis_get_devices':瓦里迪克
功能不可用
你知道我该怎么解决吗?
C函数:

/// Get device list. Takes a list of model ended by zero.
EVOLIS_LIB int evolis_get_devices(evolis_device_t** devices, ...);

SWIFT功能:

public static func getDevices() -> [Device] {
    var devices: UnsafeMutablePointer<evolis_device_t>?
    var count: Int32 = 0

    evolis_get_devices(&devices, count) // <- 'evolis_get_devices' is unavailable: Variadic

    var devicesList: [Device] = []

    if count > 0 {
        for i in 0...count - 1 {
            let device = Device(device_c: devices![Int(i)])
            devicesList.append(device)
        }
    }

    evolis_free_devices(devices)

    return devicesList
}
w46czmvw

w46czmvw1#

我在评论中链接的Apple doc特别指出:
Swift只导入使用va_list作为参数的C变量函数,不导入使用...语法作为变量参数的C函数,因此不能使用CVarArg参数调用。
因此,您会看到以下错误:
无法使用'evolis_get_devices':瓦里迪克
理想情况下,您的C函数应该具有va_list等效项,类似于:

void NSLog(NSString *format, ...);

具有匹配的:

void NSLogv(NSString *format, va_list args);

但不能保证它确实存在,即使在纯C语言中,一个变量函数也不能通过“传递”参数来调用另一个变量函数,参见问题here
让我们考虑一下可能的解决方法:
1.*C Package 函数 *,带有预定义数量的参数,用于调用变量函数。这些参数可以暴露给Swift。我们可以有多个带有不同数量参数的定义,但从长远来看,这是不可维护的,并且会用冗余条目污染Swift符号空间。
1.使用Swift的convention(c)unsafeBitCast调用带有任意数量参数的C。就像我的答案herehere一样。本质上,这将问题转移到Swift,我们仍然只能有预定义数量的参数,不能再多一个。大致看起来如下:

func getDevices()(devices: OpaquePointer...) -> Int {
    let cIMP = unsafeBitCast(evolis_get_devices, to: IMP.self)

    switch devices.count {
    case 1:
        return unsafeBitCast(fakeIMP,to:(@convention(c)(OpaquePointer) -> Int).self)(devices[0))
    case 2:
        return unsafeBitCast(fakeIMP,to:(@convention(c)(OpaquePointer, OpaquePointer) -> Int).self)(devices[0), devices[1])

     // ... and the predefined hardcoded list would keep growing to as many args as we need
    }
}

1.* 假设 * 我们可以得到一个 *C函数 Package 器 *,其中包含va_list参数,如NSLogv中的参数,该参数将调用变元真函数。
让我们考虑一下va_list实际上是什么,它是一个指向动态集合的指针,原始参数存储在该集合中,问题是,每个参数的确切位置信息并没有存储在va_list中,编译器有责任在调用变量函数时遵守cpu平台和操作系统的ABI规则。但是还有更多的挑战,C语言没有假设如何处理这些参数,甚至没有假设何时停止处理。为了说明问题,让我们比较一下C函数的签名:

/// Get device list. Takes a list of model ended by zero.
EVOLIS_LIB int evolis_get_devices(evolis_device_t** devices, ...);

对比

int printf ( const char * format, ... );

printf使用格式字符串来知道在哪里停止,但是根据文档,你的函数需要一个终止的NULL参数。仅仅通过查看va_list,没有办法知道这一点,信息不存在。只有基于规范,人们才能假设函数如何工作以及如何使用,但是C语言本身没有办法弄清楚这一点。
这给我们留下了一个重要的结论--我们的 *C函数 Package 器 *(带有va_list)需要做这个假设。
让我们再退一步来分析变量函数被调用时究竟发生了什么。参数处理实际上是如何工作的?为了我们的分析,我想出了一个函数来模仿你的(有点),我不知道evolis_device_t**到底是什么,所以我选择了void**

int evolis_get_devices(void** devices, ...) {
    /* pointer to the variable arguments list */
    va_list pargs;

    /* Initialise pargs to point to the first optional argument */
    va_start(pargs, devices);

    int numberOfArgumentsProcessed = 0;
    void** currentVargsPointer = devices;
    while( currentVargsPointer ) {
        // there are probably ways to silence the warning here but I didn't bother
        printf("%lx\n", currentVargsPointer);
        // position next va_list entry
        currentVargsPointer = va_arg(pargs, void**);
        numberOfArgumentsProcessed++;

    }
    va_end(pargs);
    return numberOfArgumentsProcessed;
}

所以变量参数void** devices, ...通过va_start(pargs, devices)的方式变成了va_list。我们实际上不需要详细说明它是如何工作的。在我的测试函数中,我打印出到达的指针变量,返回值是到达的参数总数。我想象你的真实的函数实际上可能会返回一些状态代码作为int。然而,我们的测试样本主要是关于有一种方法来检查什么是去。
过去,在栈上传递参数是在可用硬件上的标准方式。古老的C语言根本没有指定参数签名,作为一个副作用,参数的数量也没有限制。由于每个编译器都可以这样做,它就一直存在,后来成为一种语言特性。
现在,现代的CPU都配备了硬件寄存器,相比之下,堆栈操作不再那么快了。这就是现代ABI定义混合方法的原因--从寄存器开始,当我们用完硬件寄存器时,使用推到堆栈上的参数。
CPU之间明显不同,例如,Intel与Arm 64相比有不同的寄存器集和指令。但这些细微差别并不止于此,甚至操作系统偶尔也会引入自己的规则。
这让我们得出另一个非常重要的结论--无论我们做什么可移植性都会受到影响,因为我们的目标是一些连C语言自己都无法表达的东西。因此,它伴随着硬件和操作系统特定的内联 * 汇编 *。
我按照stackoverflow的标准写了这篇冗长的介绍,因为我想让你清楚地知道我在说什么。下面是我用va_listint evolis_get_devices(void** devices, ...)写的一个C函数 Package 器。我做了以下假设:

  • 它支持整数/指针参数,不支持其他参数
  • 它在X86-64和ARM 64上受支持,因为Swift实际上在
  • Apple平台优先作为我的目标环境,Linux和Windows变体是一个飞跃,而且 * 未经我测试 *
  • 最后一个参数假定为NULL,例如evolis_get_devices(ptr, NULL)
  • 它使用void**作为arg类型,因为C中所有指针的处理完全相同,所以evolis_device_t**void**没有区别

这个C代码是XCode兼容的。这是Swift的桥接头最终需要的wrapper.h

#ifndef example_h
#define example_h

#include <stdio.h>
#include <stdarg.h>

#endif /* example_h */

int evolis_get_devices_using_va_list(va_list args);

这就是我在wrapper.c中的int evolis_get_devices_using_va_list(va_list args)函数:

#include "wrapper.h"
#include <stdbool.h>

#define STACK_NUMBER_OF_ARGS_MAXIMUM 256 // keep this multiple of 32 ( for arm 32 byte chunk processing)
#define STACK_ARGS_MAXIMUM_SIZE_IN_BYTES 8 * STACK_NUMBER_OF_ARGS_MAXIMUM

// from https://stackoverflow.com/a/5459929/5329717
#define STR_HELPER(x) #x
// c Preprocessor conversion from int to string
#define STR(x) STR_HELPER(x)

#if defined(__APPLE__)
#define C_SYMBOLS_LINKER_PREFIX "_"
#elif
#define C_SYMBOLS_LINKER_PREFIX ""
#endif
int evolis_get_devices_using_va_list(va_list args) {
    register int argsOnStackCounter
#if defined(__arm64__)
    asm("x8")
#elif __x86_64__
    asm("r11")
#endif
    = 0;

    void** stackVarsMemoryBuffer[STACK_NUMBER_OF_ARGS_MAXIMUM];
    register void* inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue
#if defined(__arm64__)
    asm("x9")
#elif __x86_64__
    asm("rax")
#endif
    = (void*)stackVarsMemoryBuffer;

    register void** arg1_arm64_outputReturnValue
#if defined(__arm64__)
    asm("x0")
#elif __x86_64__
    asm("rdi")
#endif
    = 0;
#if __x86_64__
    register void** arg2 asm("rsi") = 0;
#elif defined(__arm64__) && !defined(__APPLE__)
    register void** arg2 asm("x1") = 0;
#endif

#if __x86_64__
    register void** arg3 asm("rdx") = 0;
#elif defined(__arm64__) && !defined(__APPLE__)
    register void** arg3 asm("x2") = 0;
#endif

#if __x86_64__
    register void** arg4 asm("rcx") = 0;
#elif defined(__arm64__) && !defined(__APPLE__)
    register void** arg4 asm("x3") = 0;
#endif

#if __x86_64__
    register void** arg5 asm("r8") = 0;
#elif defined(__arm64__) && !defined(__APPLE__)
    register void** arg5 asm("x4") = 0;
#endif

#if __x86_64__
    register void** arg6 asm("r9") = 0;
#elif defined(__arm64__) && !defined(__APPLE__)
    register void** arg6 asm("x5") = 0;
#endif

#if defined(__arm64__) && !defined(__APPLE__)
    register void** arg7 asm("x6") = 0;
    register void** arg8 asm("x7") = 0;
#endif

    void** currentVargsPointer;
    int argsDepth = 0;
    do {
        // position on next va_list entry
        currentVargsPointer = va_arg(args, void**);
        switch (argsDepth) {
        case 0:
            arg1_arm64_outputReturnValue = currentVargsPointer;
            break;
#if __x86_64__ || (defined(__arm64__) && !defined(__APPLE__))
        case 1:
            arg2 = currentVargsPointer;
            break;
        case 2:
            arg3 = currentVargsPointer;
            break;
        case 3:
            arg4 = currentVargsPointer;
            break;
        case 4:
            arg5 = currentVargsPointer;
            break;
        case 5:
            arg6 = currentVargsPointer;
            break;
#endif
#if defined(__arm64__) && !defined(__APPLE__)
        case 6:
            arg7 = currentVargsPointer;
            break;
        case 8:
            arg7 = currentVargsPointer;
            break;
#endif
        default:
            // argument on stack prep
            stackVarsMemoryBuffer[argsOnStackCounter] = currentVargsPointer;
            argsOnStackCounter++;
            break;
        }
        argsDepth++;
    } while( currentVargsPointer );
#if defined(__arm64__)
    asm volatile(// reserve space on stack
                 "sub    sp, sp," STR(STACK_ARGS_MAXIMUM_SIZE_IN_BYTES) "\t\n"
                 // calculate how many 32 byte chunks we need for x8 amount of stack args
                 "lsr    x8,x8, #2 \t\n" //bit shift right equivalent to int division x8 / 8
                 "add    x8,x8, #1 \t\n"
                 // use another register for destination to keep the original sp intact
                 "mov    x10, sp      \t\n"
                 // copy in 32 byte chunks, after that shift source and destination +32
                 "1:ldp  q0, q1, [x9], #32 \t\n"
                 "stp    q0, q1, [x10], #32 \t\n"
                 // move loop iterator by 1
                 "sub    w8, w8, #1   \t\n"
                 // check if 32 byte chunks are left to handle
                 "cmp    w8, #0       \t\n"
                 // jmp backwards if there are chunks left to process
                 "b.hi   1b           \t\n"
                 // we are done, time to call
                 "2:     bl " C_SYMBOLS_LINKER_PREFIX "evolis_get_devices \t\n"
                 // restore stack to entry state
                 "add    sp, sp," STR(STACK_ARGS_MAXIMUM_SIZE_IN_BYTES) "\t\n"
                 // mark the outputs:
                 // x0     it's the return value of the function we call
                 // "dirty" outputs, not really needed for anything afterwards:
                 // x8     argsOnStackCounter
                 // x9     inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue
                 :"=r"(inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue), "=r"(argsOnStackCounter), "=r"(arg1_arm64_outputReturnValue)
    #if !defined(__APPLE__)
                 // potential callee outputs under AARCH64 ABI for Windows & Linux
                 //    x1        former arg2
                 //    x2        former arg3
                 //    x3        former arg4
                 //    x4        former arg5
                 //    x5        former arg6
                 //    x6        former arg7
                 //    x7        former arg8
                 , "=r"(arg2), "=r"(arg3), "=r"(arg4), "=r"(arg5), "=r"(arg6), "=r"(arg7), "=r"(arg8)
    #endif
                 // mark the inputs
                 // x0     arg1
                 // x8     argsOnStackCounter
                 // x9     inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue
                 :"r"(arg1_arm64_outputReturnValue),"r"(inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue),"r"(argsOnStackCounter)
    #if !defined(__APPLE__)
                 // more register input args on Windows & Linux Aarch64 ABIs
                 //    x1        arg2
                 //    x2        arg3
                 //    x3        arg4
                 //    x4        arg5
                 //    x5        arg6
                 //    x6        arg7
                 //    x7        arg8
                 , "r"(arg2), "r"(arg3), "r"(arg4), "r"(arg5), "r"(arg6), "r"(arg7), "r"(arg8)
    #endif
                 // clobbered registers
                 : // by us and potentially by callee
                 "x10","x11","x12",
                 // v0, v1 those are synonyms for q0,q1 that do work in XCode's clang inline assembly in the clobber list, see https://stackoverflow.com/a/19984325/5329717
                 "v0","v1",
                 // clobbered potentially by the callee
    #if defined(__APPLE__)
                 "x2","x3","x4","x5","x6","x7",
    #endif
                 "x13","x14","x15");
#elif __x86_64__
     asm volatile(".intel_syntax noprefix \t\n"
                  // reserve space on stack for the args of the call
                  "sub rsp," STR(STACK_ARGS_MAXIMUM_SIZE_IN_BYTES) "\t\n"
                  // make a copy of rsi,rdi,rcx representing args in registers
                  "push rsi \t\n"
                  "push rdi \t\n"
                  "push rcx \t\n"
                  // position rdi on the original reserved space on stack
                  // adjusted +24 because stack grew "downwards" when rsi,rdi,rcx got pushed
                  "lea rdi, [rsp + 24] \t\n"
                  // position rsi on the inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue
                  "mov rsi, rax \t\n"
                  // put number of arg stack arguments into ecx
                  "mov ecx, r11d \t\n"
                  // copy (stack args number) * 8 bytes from rsi to rdi
                  "rep movsq \t\n"
                  // restore rsi,rdi,rcx representing args in registers
                  "pop rcx \t\n"
                  "pop rdi \t\n"
                  "pop rsi \t\n"
                  // args are prepared, we are almost ready to call
                  // by AMD64 ABI convention register al will denote the number of float arguments in variadic call, zero in our case
                  "xor eax,eax \t\n"
                  // make the actual call to evolis_get_devices(...)
                  "call " C_SYMBOLS_LINKER_PREFIX "evolis_get_devices \t\n"
                  // restore stack to entry state
                  "add rsp," STR(STACK_ARGS_MAXIMUM_SIZE_IN_BYTES) "\t\n"
                  // mark the outputs:
                  //    rax        it's the return value of the function we call
                  // "dirty" outputs, not really needed for anything afterwards:
                  //    r11        can be clobbered by the callee
                  // potential callee outputs under AMD64 ABI
                  //    rdi        former arg1
                  //    rsi        former arg2
                  //    rdx        former arg3
                  //    rcx        former arg4
                  //    r8         former arg5
                  //    r9         former arg6
                  :"=r"(inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue), "=r"(argsOnStackCounter), "=r"(arg1_arm64_outputReturnValue), "=r"(arg2),"=r"(arg3),"=r"(arg4),"=r"(arg5),"=r"(arg6)
                  // mark the inputs:
                  //    rdi        arg1
                  //    rsi        arg2
                  //    rdx        arg3
                  //    rcx        arg4
                  //    r8         arg5
                  //    r9         arg6
                  //    rax        inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue
                  //    r11        argsOnStackCounter
                  :"r"(arg1_arm64_outputReturnValue), "r"(arg2), "r"(arg3), "r"(arg4), "r"(arg5), "r"(arg6), "r"(inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue), "r"(argsOnStackCounter)
                  // clobbered registers (potentially by the callee)
                  : "r10"
                  );
#endif

// there are probably ways to silence the warning here but I didn't bother
#if __x86_64__
    return (int)inputStackVarsMemoryBufferPointer_x86_64_outputReturnValue;
#elif defined(__arm64__)
    return (int)arg1_arm64_outputReturnValue;
#endif
}

这是我在Swift中调用的测试代码:

let args: [OpaquePointer] = [ OpaquePointer.init(bitPattern: 1)!,
                                      OpaquePointer.init(bitPattern: 2)!,
                                      OpaquePointer.init(bitPattern: 3)!,
                                      OpaquePointer.init(bitPattern: 4)!,
                                      OpaquePointer.init(bitPattern: 5)!,
                                      OpaquePointer.init(bitPattern: 6)!,
                                      OpaquePointer.init(bitPattern: 7)!,
                                      OpaquePointer.init(bitPattern: 8)!,
                                      OpaquePointer.init(bitPattern: 9)!
        ]
        withVaList(args + [Int(0)], {
            print("Number of args processed:",evolis_get_devices_using_va_list($0))
        })

当然,也可以使用Swift可变参数函数:

func variadicSwiftFunction(devices: UnsafeMutablePointer<OpaquePointer>...) {
        withVaList(devices + [Int(0)], {
            print("Number of args processed:",evolis_get_devices_using_va_list($0))
        })
    }

在Swift代码中,注意最后一个参数NULL是通过看起来很奇怪的Int(0)传递的。

相关问题