Swift 对字符串 CVarArg 的处理有问题吗?

Is Swift's handling of CVarArg for String buggy?

在为 C++ 库的 C 包装器编写 Swift 包装器时,我偶然发现了一些关于 Swift 的 CVarArg 的奇怪错误。我已经拥有的 C 包装器使用可变参数函数,我使用 va_list 作为参数将其转换为函数,因此可以导入它们(因为 Swift 无法导入 C 可变参数函数)。将参数传递给此类函数时,一旦桥接到 Swift,它就会使用符合 CVarArg 类型的私有 _cVarArgEncoding 属性 来“编码”随后发送的值作为指向 C 函数的指针。然而,对于 Swift Strings.

,这种编码似乎是错误的

为了演示,我创建了以下包:

Package.swift

// swift-tools-version:5.2

import PackageDescription

let package = Package(
    name: "CVarArgTest",
    products: [
        .executable(
            name: "CVarArgTest",
            targets: ["CVarArgTest"]),
    ],
    targets: [
        .target(
            name: "CLib"),
        .target(
            name: "CVarArgTest",
            dependencies: ["CLib"])
    ]
)

CLib

CTest.h

#ifndef CTest_h
#define CTest_h

#include <stdio.h>

/// Prints out the strings provided in args
/// @param num The number of strings in `args`
/// @param args A `va_list` of strings
void test_va_arg_str(int num, va_list args);

/// Prints out the integers provided in args
/// @param num The number of integers in `args`
/// @param args A `va_list` of integers
void test_va_arg_int(int num, va_list args);

/// Just prints the string
/// @param str The string
void test_str_print(const char * str);

#endif /* CTest_h */


CTest.c

#include "CTest.h"
#include <stdarg.h>

void test_va_arg_str(int num, va_list args)
{
    printf("Printing %i strings...\n", num);
    for (int i = 0; i < num; i++) {
        const char * str = va_arg(args, const char *);
        puts(str);
    }
}

void test_va_arg_int(int num, va_list args)
{
    printf("Printing %i integers...\n", num);
    for (int i = 0; i < num; i++) {
        int foo = va_arg(args, int);
        printf("%i\n", foo);
    }
}

void test_str_print(const char * str)
{
    puts(str);
}

main.swift

import Foundation
import CLib

// The literal String is perfectly bridged to the CChar pointer expected by the function
test_str_print("Hello, World!")

// Prints the integers as expected
let argsInt: [CVarArg] = [123, 456, 789]
withVaList(argsInt) { listPtr in
    test_va_arg_int(Int32(argsInt.count), listPtr)
}

// ERROR: Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
let argsStr: [CVarArg] = ["Test", "Testing", "The test"]
withVaList(argsStr) { listPtr in
    test_va_arg_str(Int32(argsStr.count), listPtr)
}

包也可用 here

如上面代码中所述,通过 C 打印 String 或包含 Ints 的 va_list 按预期工作,但是当转换为 const char * 时,有异常 (EXC_BAD_ACCESS (code=EXC_I386_GPFLT)).

所以,简而言之:是我搞砸了它的 C 端还是 Swift 在这里做错了什么?我已经在 Xcode 11.5 和 12.0b2 中对此进行了测试。如果这是一个错误,我很乐意报告。

这个有点棘手:你的字符串实际上被桥接到 Objective-C NSString * 而不是 C char *:

(lldb) p str
(const char *) [=10=] = 0x3cbe9f4c5d32b745 ""
(lldb) p (id)str
(NSTaggedPointerString *)  = 0x3cbe9f4c5d32b745 @"Test"

(如果您想知道为什么它是 NSTaggedPointerString 而不仅仅是 NSStringthis article 是一个很好的读物——简而言之,字符串足够短,可以直接存储在指针变量的字节中,而不是堆上的对象中。

正在查看 the source code for withVaList, we see that a type's va_list representation is determined by its implementation of the _cVarArgEncoding property of the CVarArg protocol. The standard library has some implementations of this protocol for some basic integer and pointer types,但此处 String 没有任何内容。那么谁将我们的字符串转换为 NSString?

在 GitHub 上搜索 Swift 回购,我们发现 Foundation is the culprit:

//===----------------------------------------------------------------------===//
// CVarArg for bridged types
//===----------------------------------------------------------------------===//
extension CVarArg where Self: _ObjectiveCBridgeable {
  /// Default implementation for bridgeable types.
  public var _cVarArgEncoding: [Int] {
    let object = self._bridgeToObjectiveC()
    _autorelease(object)
    return _encodeBitsAsWords(object)
  }
}

用简单的英语来说:任何可以桥接到 Objective-C 的对象都通过转换为 Objective-C 对象并编码指向该对象的指针来编码为可变参数。 C 可变参数不是类型安全的,所以你的 test_va_arg_str 只是假设它是一个 char* 并将它传递给 puts,这会崩溃。

这是一个错误吗?我不这么认为——我想这种行为可能是为了与 NSLog 之类的函数兼容,这些函数更常用于 Objective-C 对象而不是 C 对象。然而,这确实是一个令人惊讶的陷阱,这可能是 Swift 不喜欢让你调用 C 可变参数函数的原因之一。


您需要通过手动将字符串转换为 C 字符串来解决此问题。如果您有一个要转换的字符串数组而不进行不必要的复制,这可能会有点难看,但这里有一个函数应该能够做到这一点。

extension Collection where Element == String {
    /// Converts an array of strings to an array of C strings, without copying.
    func withCStrings<R>(_ body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
        return try withCStrings(head: [], body: body)
    }
    
    // Recursively call withCString on each of the strings.
    private func withCStrings<R>(head: [UnsafePointer<CChar>],
                                 body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
        if let next = self.first {
            // Get a C string, add it to the result array, and recurse on the remainder of the collection
            return try next.withCString { cString in
                var head = head
                head.append(cString)
                return try dropFirst().withCStrings(head: head, body: body)
            }
        } else {
            // Base case: no more strings; call the body closure with the array we've built
            return try body(head)
        }
    }
}

func withVaListOfCStrings<R>(_ args: [String], body: (CVaListPointer) -> R) -> R {
    return args.withCStrings { cStrings in
        withVaList(cStrings, body)
    }
}

let argsStr: [String] = ["Test", "Testing", "The test"]
withVaListOfCStrings(argsStr) { listPtr in
    test_va_arg_str(Int32(argsStr.count), listPtr)
}

// Output:
// Printing 3 strings...
// Test
// Testing
// The test