在为C++库的C包装器编写Swift包装器时,我偶然发现了一些关于Swift的CVarArg
的奇怪错误。我已经拥有的C包装器使用了变元函数,我将其转换为使用va_list
作为参数的函数,以便可以导入它们(因为Swift无法导入C变元函数(。当将参数传递给这样一个函数时,一旦桥接到Swift,它就会使用符合CVarArg
到"类型的私有_cVarArgEncoding
属性;编码";然后将这些值作为指向C函数的指针发送。然而,SwiftString
s的这种编码似乎有问题。
为了演示,我创建了以下包:
软件包.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
C测试小时
#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 */
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("%in", 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)
}
这里也提供包裹。
正如上面代码中所评论的,通过C打印String
或包含Int
的va_list
可以按预期工作,但当转换为const char *
时,会出现异常(EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
(。
简言之:是我把C端搞砸了,还是斯威夫特在这里做错了什么?我已经在Xcode 11.5和12.0b2中测试过了。如果这是一个错误,我很乐意报告。
这个有点棘手:您的字符串实际上被桥接到Objective-CNSString *
,而不是Cchar *
:
(lldb) p str
(const char *) $0 = 0x3cbe9f4c5d32b745 ""
(lldb) p (id)str
(NSTaggedPointerString *) $1 = 0x3cbe9f4c5d32b745 @"Test"
(如果你想知道为什么它是NSTaggedPointerString
而不仅仅是NSString
,这篇文章读起来很好——简而言之,字符串足够短,可以直接存储在指针变量的字节中,而不是堆上的对象中。
查看withVaList
的源代码,我们可以看到类型的va_list
表示是由其实现CVarArg
协议的_cVarArgEncoding
属性决定的。对于一些基本的整数和指针类型,标准库有一些该协议的实现,但这里没有String
。那么谁在把我们的字符串转换成NSString
呢?
在GitHub上搜索Swift回购,我们发现Foundation是罪魁祸首:
//===----------------------------------------------------------------------===//
// 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的对象都被编码为vararg,方法是转换为Objective-C对象并编码指向该对象的指针。C varargs不是类型安全的,所以您的test_va_arg_str
只是假设它是char*
,并将它传递给puts
,后者会崩溃。
那么这是一个bug吗?我不这么认为——我想这种行为可能是为了与像NSLog
这样的函数兼容,这些函数比C对象更常用于Objective-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