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 String
s.
,这种编码似乎是错误的
为了演示,我创建了以下包:
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
或包含 Int
s 的 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
而不仅仅是 NSString
,this 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
在为 C++ 库的 C 包装器编写 Swift 包装器时,我偶然发现了一些关于 Swift 的 CVarArg
的奇怪错误。我已经拥有的 C 包装器使用可变参数函数,我使用 va_list
作为参数将其转换为函数,因此可以导入它们(因为 Swift 无法导入 C 可变参数函数)。将参数传递给此类函数时,一旦桥接到 Swift,它就会使用符合 CVarArg
类型的私有 _cVarArgEncoding
属性 来“编码”随后发送的值作为指向 C 函数的指针。然而,对于 Swift String
s.
为了演示,我创建了以下包:
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
或包含 Int
s 的 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
而不仅仅是 NSString
,this 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