【问题标题】:Converting a C char array to a String将 C 字符数组转换为字符串
【发布时间】:2015-02-11 21:07:30
【问题描述】:

我有一个与 C 库互操作的 Swift 程序。这个 C 库返回一个内部带有 char[] 数组的结构,如下所示:

struct record
{
    char name[8];
};

定义已正确导入 Swift。但是,该字段被解释为 8 个 Int8 元素(类型为 (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8))的 元组,我不知道如何使用 Swift 将其转换为 String

没有接受Int8 元组的String 初始化程序,并且似乎不可能获得指向元组第一个元素的指针(因为类型可以是异构的,这并不奇怪)。

现在,我最好的想法是创建一个微型 C 函数,它接受指向结构本身的指针并将 name 作为 char* 指针而不是数组返回,然后继续。

但是,有没有纯 Swift 的方式来做到这一点?

【问题讨论】:

  • 你确定互操作使它成为一个 C 问题吗?或者你的解决方法是这样做的?特别是当你想要一个纯粹的快速解决方案时......
  • @Deduplicator,如果我正在寻找如何将 C char 数组转换为 Swift 字符串,我肯定会寻找标签“c”和“swift”。
  • 该字节数组没有任何 C 语言,但您对它的描述在 C、C++、objective-C、objective-C++ 等中有效。不是 C 题。
  • 我不知道有人将这些称为“C++ 数组”或“Objective-C 数组”或“Objective-C++ 数组”,我也不知道“C 数组”的其他定义。当我寻找解决方案时,我在搜索词中使用了“C 数组”,除非我是异常值,否则我相信下一个遇到相同问题的人也会这样做。我认为标签对于搜索请求来说是最重要的,它们的分类目的是其次的。
  • 如果经常使用 C 作为低级和本机的同义词来描述的任何东西都被标记为 C,那么几乎所有与本机互操作有关的东西都会淹没 C 标记。非常糟糕的主意。

标签: arrays swift interop tuples


【解决方案1】:

C 数组 char name[8] 作为元组导入 Swift:

(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)

name的地址与name[0]的地址相同,且 Swift 保留从 C 导入的结构的内存布局,如 confirmed by Apple engineer Joe Groff:

... 您可以保留在 C 中定义的结构并将其导入 Swift。 Swift 会尊重 C 的布局。

因此,我们可以传递record.name的地址, 转换为 UInt8 指针,到 字符串初始值设定项。以下代码已针对 Swift 4.2 及更高版本进行了更新:

let record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
        String(cString: $0)
    }
}

注意:假定name[] 中的字节是有效的以NUL 结尾的UTF-8 序列。

对于旧版本的 Swift:

// Swift 2:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(&record.name) {
    String.fromCString(UnsafePointer($0))!
}

// Swift 3:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: &record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: record.name)) {
        String(cString: $0)
    }
}

【讨论】:

  • 是的,这行得通。我将添加 record 需要 是可变的(用 var 声明),否则 Swift 编译器会出现奇怪的错误。
  • @zneak:你说得对,我已将这些信息添加到答案中。
  • 我相信这会起作用,但它是否记录了元组在内存中是连续的?使用数组,除非您调用 withUnsafeBufferPointer,否则您不会得到保证,但据我所知,尚未记录元组实现。
  • @NateCook:实际上,如果结构是从 C 中导入的,它保证的。我已经通过参考相应地更新了答案,并简化了代码。
  • @MechEthan:感谢您的通知!还有另一种解决方案,因为(从 Swift 4.2 开始)您可以获取不可变值的地址,即您可以将 withUnsafePointer(to:) 与常量值一起使用,这也避免了同时(变异)访问的问题。我已经相应地更新了代码。
【解决方案2】:

关于这个主题已经有多个答案,但没有一个是简单的一行,也没有解决非空终止的问题。

假设字符串是 NULL 终止:

struct record {
    char name[8];
};

//Might by unsafe, depends
String(cString: &record.name.0)

//Safe
String(cString: unsafeBitCast(UnsafePointer(&record.name), to: UnsafePointer<Int8>.self))

对于不是NULL 终止的字符串:

//Might by unsafe, depends
String(cString: &record.name.0).prefix(MemoryLayout.size(ofValue: record.name))

//Safe
String(bytesNoCopy: UnsafeMutableRawPointer(mutating: &record.name), length: MemoryLayout.size(ofValue: record.name), encoding: .utf8, freeWhenDone: false)

––––

关于 @MartinR 只传递一个字节的问题,你也可以传递一个指向整个变量的指针,但就个人而言,我从未体验过只传递一个字节的 swift,所以它应该是安全的。

【讨论】:

  • 请注意,您的解决方案中有两个方面的未定义行为。首先,您只将一个字符作为 inout 表达式传递给 String 初始化程序,在此处比较 OOP 的注释:stackoverflow.com/a/41599023/1187415。其次,如果字符串不是以 NULL 结尾的,那么您可能会读取未定义内存的内容。
  • @MartinR 那么如何将 ref 传递给整个变量呢?
  • 应该工作,并且很可能没有运行时差异。我仍然更喜欢withMemoryRebound 而不是unsafeBitCast,它被记录为“当无法通过其他方式进行转换时,仅使用此函数将作为x 传递的实例转换为布局兼容的类型。 ... 调用这个函数破坏了 Swift 类型系统的保证;使用时要格外小心。”
  • @MartinR 关于非空终止字符串,是的,确实,人们应该谨慎使用我的方法,在某些情况下它可能是危险的,但在其他情况下,比如我的,它是完全安全的。我有一个带有两个 char[] 的结构,然后是其他一些东西。在数组和其他东西之间有一个填充,并且结构在填充之前是 0,所以它保证它将停止读取那里并且不会读取未分配的内存。
【解决方案3】:

详情

  • Xcode 11.2.1 (11B500)、Swift 5.1

解决方案

extension String {
    init?(fromTuple value: Any) {
        guard let string = Tuple(value).toString() else { return nil }
        self = string
    }

    init?(cString: UnsafeMutablePointer<Int8>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init?(cString: UnsafeMutablePointer<CUnsignedChar>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init? (cString: Any) {

        if let pointer = cString as? UnsafeMutablePointer<CChar> {
            self = String(cString: pointer)
            return
        }

        if let pointer = cString as? UnsafeMutablePointer<CUnsignedChar> {
            self = String(cString: pointer)
            return
        }

        if let string = String(fromTuple: cString) {
            self = string
            return
        }

        return nil
    }
}

// https://stackoverflow.com/a/58869882/4488252

struct Tuple<T> {
    let original: T
    private let array: [Mirror.Child]
    init(_ value: T) {
        self.original = value
        array = Array(Mirror(reflecting: original).children)
    }
    func compactMap<V>(_ transform: (Mirror.Child) -> V?) -> [V] { array.compactMap(transform) }

    func toString() -> String? {

        let chars = compactMap { (_, value) -> String? in
            var scalar: Unicode.Scalar!
            switch value {
            case is CUnsignedChar: scalar = .init(value as! CUnsignedChar)
            case is CChar: scalar = .init(UInt8(value as! CChar))
            default: break
            }
            guard let _scalar = scalar else { return nil }
            return String(_scalar)
        }
        if chars.isEmpty && !array.isEmpty { return nil }
        return chars.joined()
    }
}

用法(完整示例)

C 语言代码 (Header.h)

#ifndef Header_h
#define Header_h

#ifdef __cplusplus
extern "C" {
#endif

char c_str1[] = "Hello world!";
char c_str2[50] = "Hello world!";
char *c_str3 = c_str2;

typedef unsigned char UTF8CHAR;
UTF8CHAR c_str4[] = {72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0};
UTF8CHAR *c_str5 = c_str4;
UTF8CHAR c_str6[] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\0'};
UTF8CHAR *c_str7 = 0;
UTF8CHAR *c_str8 = "";

#define UI BYTE

#ifdef __cplusplus
}
#endif

#endif /* Header_h */

...-Bridging-Header.h

#include "Header.h"

Swift 代码

func test() {
    printInfo(c_str1)
    printInfo(c_str2)
    printInfo(c_str3)
    printInfo(c_str4)
    printInfo(c_str5)
    printInfo(c_str6)
    printInfo(c_str7)
    printInfo(c_str8)

    print(String(fromTuple: c_str1) as Any)
    print(String(fromTuple: c_str2) as Any)
    print(String(cString: c_str3) as Any)
    print(String(fromTuple: c_str4) as Any)
    print(String(cString: c_str5) as Any)
    print(String(fromTuple: c_str6) as Any)
    print(String(fromTuple: c_str7) as Any)
    print(String(cString: c_str8) as Any)
}

var counter = 1;

func printInfo(_ value: Any?) {
    print("name: str_\(counter)")
    counter += 1
    guard let value = value else { return }
    print("type: \(type(of: value))")
    print("value: \(value)")
    print("swift string: \(String(cString: value))")
    print("\n-----------------")
}

输出

name: str_1
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_2
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
swift string: Optional("Hello world!\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")

-----------------
name: str_3
type: UnsafeMutablePointer<Int8>
value: 0x000000010e8c5d40
swift string: Optional("Hello world!")

-----------------
name: str_4
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0)
swift string: Optional("Hello world \0")

-----------------
name: str_5
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c5d80
swift string: Optional("Hello world ")

-----------------
name: str_6
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_7
name: str_8
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c0ae0
swift string: Optional("")

-----------------
Optional("Hello world!\0")
Optional("Hello world!\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
Optional("Hello world!")
Optional("Hello world \0")
Optional("Hello world ")
Optional("Hello world!\0")
Optional("")
Optional("")

【讨论】:

    【解决方案4】:

    我刚刚在使用 Swift 3. (3.0.2) 时遇到了类似的问题。我试图将 CChar 数组 [CChar] 转换为 Swift 中的字符串。事实证明 Swift 3 有一个 String 初始化器,它将接受一个 cString。

    例子:

    let a = "abc".cString(using: .utf8) // type of a is [CChar]
    let b = String(cString: a!, encoding: .utf8) // type of b is String
    print("a = \(a)")
    print("b = \(b)")
    

    结果

    a = 可选([97, 98, 99, 0])

    b = 可选(“abc”)

    请注意,String 上的 cString 函数会导致 Optional。在创建 b 的 String.init 函数中使用时必须强制解包。而且 b 也是可选的...意味着两者都可能最终为零,因此还应该使用错误检查。

    【讨论】:

      【解决方案5】:

      Swift 3. 只使用反射。此版本在遇到空字节时停止构建字符串。经过测试。

      func TupleOfInt8sToString( _ tupleOfInt8s:Any ) -> String? {
          var result:String? = nil
          let mirror = Mirror(reflecting: tupleOfInt8s)
      
          for child in mirror.children {
              guard let characterValue = child.value as? Int8, characterValue != 0 else {
                  break
              }
      
              if result == nil {
                  result = String()
              }
              result?.append(Character(UnicodeScalar(UInt8(characterValue))))
          }
      
          return result
      }
      

      【讨论】:

        【解决方案6】:

        这是我想出的一个解决方案,它使用反射将元组实际转换为 [Int8](请参阅 Any way to iterate a tuple in swift?),然后使用 fromCString...() 方法将其转换为字符串。

        func arrayForTuple<T,E>(tuple:T) -> [E] {
            let reflection = reflect(tuple)
            var arr : [E] = []
            for i in 0..<reflection.count {
                if let value = reflection[i].1.value as? E {
                    arr.append(value)
                }
            }
            return arr
        }
        
        public extension String {
            public static func fromTuple<T>(tuple:T) -> String? {
                var charArray = arrayForTuple(tuple) as [Int8]
                var nameString = String.fromCString(UnsafePointer<CChar>(charArray))
                if nameString == nil {
                    nameString = String.fromCStringRepairingIllFormedUTF8(UnsafePointer<CChar>(charArray)).0
                }
                return nameString
            }
        }
        

        【讨论】:

          【解决方案7】:

          您实际上可以使用 Swift 的可变参数语法将元组收集到数组中:

          let record = getRecord()
          let (int8s: Int8...) = myRecord          // int8s is an [Int8]
          let uint8s = int8s.map { UInt8($0) }
          let string = String(bytes: uint8s, encoding: NSASCIIStringEncoding)
          // myString == Optional("12345678")
          

          【讨论】:

          • 看起来不错(不知道可变元组语法),尽管在 真实案例 中,元组有 32 个元素,我可以看到自己需要它更大的数组(如 128 个元素)。这将导致烦人的大类型注释。你有没有办法让它独立于元素的数量?
          • let (int8s: Int8...) 是什么构造? let (name : type) = explet name : type = expr 大体一样吗?
          • @zneak 在这种情况下,您需要内联,因为使用函数您必须输入正确数量的元素以匹配每个特定的元组。我已经修改了我的答案以显示您可以做什么。
          • @MartinR 它的名字是元组分解。您可以使用此技术以有趣的方式提取内容,尝试:let (_, second, _, fourth, theRest: Int...) = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
          • 从 Xcode 7b1 beta 中的 Swift 2 开始,这不再有效。
          【解决方案8】:

          我也有兴趣为自己的目的解决这个问题,所以我添加了一个新功能:

          func asciiCArrayToSwiftString(cString:Int8...) -> String
          {
              var swiftString = String()            // The Swift String to be Returned is Intialized to an Empty String
              var workingCharacter:UnicodeScalar = UnicodeScalar(UInt8(cString[0]))
              var count:Int = cString.count
          
              for var i:Int = 0; i < count; i++
              {
                  workingCharacter = UnicodeScalar(UInt8(cString[i])) // Convert the Int8 Character to a Unicode Scalar
                  swiftString.append(workingCharacter)             // Append the Unicode Scalar
          
              }
          
              return swiftString                     // Return the Swift String
          }
          

          我调用这个函数:

              let t:Int8 = Int8(116)
              let e:Int8 = Int8(101)
              let s:Int8 = Int8(115)
              let testCString = (t, e, s, t)
              let testSwiftString = wispStringConverter.asciiCArrayToSwiftString(testCString.0, testCString.1, testCString.2, testCString.3)
              println("testSwiftString = \(testSwiftString)")
          

          结果输出是:

          testSwiftString = 测试

          【讨论】:

            【解决方案9】:

            试试这个:

            func asciiCStringToSwiftString(cString:UnsafePointer<UInt8>, maxLength:Int) -> String
            {
                var swiftString = String()  // The Swift String to be Returned is Intialized to an Empty String
                var workingCharacter:UnicodeScalar = UnicodeScalar(cString[0])
                var count:Int = 0           // An Index Into the C String Array Starting With the First Character
            
                while cString[count] != 0             // While We Haven't reached the End of the String
                {
                    workingCharacter = UnicodeScalar(cString[count]) // Convert the ASCII Character to a Unicode Scalar
                    swiftString.append(workingCharacter)             // Append the Unicode Scalar Version of the ASCII Character
                    count++                                          // Increment the Index to Look at the Next ASCII Character
            
                    if count > maxLength                            // Set a Limit In Case the C string was Not NULL Terminated
                    {
                        if printDebugLogs == true
                        {
                            swiftString="Reached String Length Limit in Converting ASCII C String To Swift String"
                        }
                        return swiftString
                    }
                }
            
                return swiftString                     // Return the Swift String
            }
            

            【讨论】:

            • C 字符串不是UnsafePointer&lt;UInt8&gt;,它是8 个Int8 元素的元组,所以这个方法不能解决我的问题。此外,String 类有一个 init?(UTF8String: UnsafePointer&lt;CChar&gt;) 可失败初始化器,它可以完全做到这一点。
            • 在我的应用程序中,我需要记录当前的 OpenGL 版本以确保像素格式设置正确。我用 let glVersionCString:UnsafePointer = glGetString(GLenum(GL_VERSION)) 来做到这一点。编译器不允许我将 glGetString 的返回值转换为 UnsafePointer,因此我不能使用 Swift String 初始化程序。这就是这个功能的原因。
            • 我并不质疑您的功能在您的特定用例中的有用性,但它不适用于此处。这对您有用,因为 glGetString 返回一个指针。我处理的struct 有一个数组字段,它与指针字段有很大不同。正如我所说,Swift 看到的字段类型是(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8),而不是UnsafePointer&lt;UInt8&gt;
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-11-20
            • 1970-01-01
            • 2022-12-09
            • 1970-01-01
            • 2021-05-09
            相关资源
            最近更新 更多